// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

goog.provide('goog.module.ModuleManagerTest');
goog.setTestOnly('goog.module.ModuleManagerTest');

goog.require('goog.array');
goog.require('goog.functions');
goog.require('goog.module.BaseModule');
goog.require('goog.module.ModuleManager');
goog.require('goog.testing');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');



var clock;
var requestCount = 0;


function tearDown() {
  clock.dispose();
}

function setUp() {
  clock = new goog.testing.MockClock(true);
  requestCount = 0;
}

function getModuleManager(infoMap) {
  var mm = new goog.module.ModuleManager();
  mm.setAllModuleInfo(infoMap);

  mm.isModuleLoaded = function(id) {
    return this.getModuleInfo(id).isLoaded();
  };
  return mm;
}

function createSuccessfulBatchLoader(moduleMgr) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      requestCount++;
      setTimeout(goog.bind(this.onLoad, this, ids.concat(), 0), 5);
    },
    onLoad: function(ids, idxLoaded) {
      moduleMgr.beforeLoadModuleCode(ids[idxLoaded]);
      moduleMgr.setLoaded(ids[idxLoaded]);
      var idx = idxLoaded + 1;
      if (idx < ids.length) {
        setTimeout(goog.bind(this.onLoad, this, ids, idx), 2);
      }
    }
  };
}

function createSuccessfulNonBatchLoader(moduleMgr) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      requestCount++;
      setTimeout(function() {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        moduleMgr.setLoaded(ids[0]);
        if (opt_successFn) {
          opt_successFn();
        }
      }, 5);
    }
  };
}

function createUnsuccessfulLoader(moduleMgr, status) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      moduleMgr.beforeLoadModuleCode(ids[0]);
      setTimeout(function() { opt_errFn(status); }, 5);
    }
  };
}

function createUnsuccessfulBatchLoader(moduleMgr, status) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      setTimeout(function() { opt_errFn(status); }, 5);
    }
  };
}

function createTimeoutLoader(moduleMgr, status) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      setTimeout(function() { opt_timeoutFn(status); }, 5);
    }
  };
}


/**
 * Tests loading a module under different conditions i.e. unloaded
 * module, already loaded module, module loaded through user initiated
 * actions, synchronous callback for a module that has been already
 * loaded. Test both batch and non-batch loaders.
 */
function testExecOnLoad() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));
  execOnLoad_(mm);

  mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulBatchLoader(mm));
  mm.setBatchModeEnabled(true);
  execOnLoad_(mm);
}


/**
 * Tests execOnLoad with the specified module manager.
 * @param {goog.module.ModuleManager} mm The module manager.
 */
function execOnLoad_(mm) {
  // When module is unloaded, execOnLoad is async.
  var execCalled1 = false;
  mm.execOnLoad('a', function() { execCalled1 = true; });
  assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
  assertTrue('module "a" should be loading', mm.isModuleLoading('a'));
  assertFalse('execCalled1 should not be set yet', execCalled1);
  assertTrue('ModuleManager should be active', mm.isActive());
  assertFalse('ModuleManager should not be user active', mm.isUserActive());
  clock.tick(5);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "a" should not be loading', mm.isModuleLoading('a'));
  assertTrue('execCalled1 should be set', execCalled1);
  assertFalse('ModuleManager should not be active', mm.isActive());
  assertFalse('ModuleManager should not be user active', mm.isUserActive());

  // When module is already loaded, execOnLoad is still async unless
  // specified otherwise.
  var execCalled2 = false;
  mm.execOnLoad('a', function() { execCalled2 = true; });
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "a" should not be loading', mm.isModuleLoading('a'));
  assertFalse('execCalled2 should not be set yet', execCalled2);
  clock.tick(5);
  assertTrue('execCalled2 should be set', execCalled2);

  // When module is unloaded, execOnLoad is async (user active).
  var execCalled5 = false;
  mm.execOnLoad('c', function() { execCalled5 = true; }, null, null, true);
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));
  assertTrue('module "c" should be loading', mm.isModuleLoading('c'));
  assertFalse('execCalled1 should not be set yet', execCalled5);
  assertTrue('ModuleManager should be active', mm.isActive());
  assertTrue('ModuleManager should be user active', mm.isUserActive());
  clock.tick(5);
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
  assertFalse('module "c" should not be loading', mm.isModuleLoading('c'));
  assertTrue('execCalled1 should be set', execCalled5);
  assertFalse('ModuleManager should not be active', mm.isActive());
  assertFalse('ModuleManager should not be user active', mm.isUserActive());

  // When module is already loaded, execOnLoad is still synchronous when
  // so specified
  var execCalled6 = false;
  mm.execOnLoad('c', function() {
    execCalled6 = true;
  }, undefined, undefined, undefined, true);
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
  assertFalse('module "c" should not be loading', mm.isModuleLoading('c'));
  assertTrue('execCalled6 should be set', execCalled6);
  clock.tick(5);
  assertTrue('execCalled6 should still be set', execCalled6);
}


/**
 * Test aborting the callback called on module load.
 */
function testExecOnLoadAbort() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  // When module is unloaded and abort is called, module still gets
  // loaded, but callback is cancelled.
  var execCalled1 = false;
  var callback1 = mm.execOnLoad('b', function() { execCalled1 = true; });
  callback1.abort();
  clock.tick(5);
  assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
  assertFalse('execCalled3 should not be set', execCalled1);

  // When module is already loaded, execOnLoad is still async, so calling
  // abort should still cancel the callback.
  var execCalled2 = false;
  var callback2 = mm.execOnLoad('a', function() { execCalled2 = true; });
  callback2.abort();
  clock.tick(5);
  assertFalse('execCalled2 should not be set', execCalled2);
}


/**
 * Test preloading modules and ensure that the before load, after load
 * and set load called are called only once per module.
 */
function testExecOnLoadWhilePreloadingAndViceVersa() {
  var mm = getModuleManager({'c': [], 'd': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));
  execOnLoadWhilePreloadingAndViceVersa_(mm);

  mm = getModuleManager({'c': [], 'd': []});
  mm.setLoader(createSuccessfulBatchLoader(mm));
  mm.setBatchModeEnabled(true);
  execOnLoadWhilePreloadingAndViceVersa_(mm);
}


/**
 * Perform tests with the specified module manager.
 * @param {goog.module.ModuleManager} mm The module manager.
 */
function execOnLoadWhilePreloadingAndViceVersa_(mm) {
  var mm = getModuleManager({'c': [], 'd': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var origSetLoaded = mm.setLoaded;
  var calls = [0, 0];
  mm.beforeLoadModuleCode = function(id) { calls[0]++; };
  mm.setLoaded = function(id) {
    calls[1]++;
    origSetLoaded.call(mm, id);
  };

  mm.preloadModule('c', 2);
  assertFalse('module "c" should not be loading yet', mm.isModuleLoading('c'));
  clock.tick(2);
  assertTrue('module "c" should now be loading', mm.isModuleLoading('c'));
  mm.execOnLoad('c', function() {});
  assertTrue('module "c" should still be loading', mm.isModuleLoading('c'));
  clock.tick(5);
  assertFalse('module "c" should be done loading', mm.isModuleLoading('c'));
  assertEquals('beforeLoad should only be called once for "c"', 1, calls[0]);
  assertEquals('setLoaded should only be called once for "c"', 1, calls[1]);

  mm.execOnLoad('d', function() {});
  assertTrue('module "d" should now be loading', mm.isModuleLoading('d'));
  mm.preloadModule('d', 2);
  clock.tick(5);
  assertFalse('module "d" should be done loading', mm.isModuleLoading('d'));
  assertTrue('module "d" should now be loaded', mm.isModuleLoaded('d'));
  assertEquals('beforeLoad should only be called once for "d"', 2, calls[0]);
  assertEquals('setLoaded should only be called once for "d"', 2, calls[1]);
}


/**
 * Tests that multiple callbacks on the same module don't cause
 * confusion about the active state after the module is finally loaded.
 */
function testUserInitiatedExecOnLoadEventuallyLeavesManagerIdle() {
  var mm = getModuleManager({'c': [], 'd': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack1 = false;
  var calledBack2 = false;

  mm.execOnLoad(
      'c', function() { calledBack1 = true; }, undefined, undefined, true);
  mm.execOnLoad(
      'c', function() { calledBack2 = true; }, undefined, undefined, true);
  mm.load('c');

  assertTrue(
      'Manager should be active while waiting for load', mm.isUserActive());

  clock.tick(5);

  assertTrue('First callback should be called', calledBack1);
  assertTrue('Second callback should be called', calledBack2);
  assertFalse(
      'Manager should be inactive after loading is complete',
      mm.isUserActive());
}


/**
 * Tests loading a module by requesting a Deferred object.
 */
function testLoad() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;
  var error = null;

  var d = mm.load('a');
  d.then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });

  assertFalse(calledBack);
  assertNull(error);
  assertFalse(mm.isUserActive());

  clock.tick(5);

  assertTrue(calledBack);
  assertNull(error);
}


/**
 * Tests loading 2 modules asserting that the loads happen in parallel
 * in one unit of time.
 */
function testLoad_concurrent() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setConcurrentLoadingEnabled(true);
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;
  var error = null;

  mm.load('a');
  mm.load('b');
  assertEquals(2, requestCount);
  // Only time for one serialized download.
  clock.tick(5);

  assertTrue(mm.getModuleInfo('a').isLoaded());
  assertTrue(mm.getModuleInfo('b').isLoaded());
}

function testLoad_concurrentSecondIsDepOfFist() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setBatchModeEnabled(true);
  mm.setConcurrentLoadingEnabled(true);
  mm.setLoader(createSuccessfulBatchLoader(mm));

  var calledBack = false;
  var error = null;

  mm.loadMultiple(['a', 'b']);
  mm.load('b');
  assertEquals('No 2nd request expected', 1, requestCount);
  // Only time for one serialized download.
  clock.tick(5);
  clock.tick(2);  // Makes second module come in from batch requst.

  assertTrue(mm.getModuleInfo('a').isLoaded());
  assertTrue(mm.getModuleInfo('b').isLoaded());
}

function testLoad_nonConcurrent() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;
  var error = null;

  mm.load('a');
  mm.load('b');
  assertEquals(1, requestCount);
  // Only time for one serialized download.
  clock.tick(5);

  assertTrue(mm.getModuleInfo('a').isLoaded());
  assertFalse(mm.getModuleInfo('b').isLoaded());
}

function testLoadUnknown() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));
  var e = assertThrows(function() { mm.load('DoesNotExist'); });
  assertEquals('Unknown module: DoesNotExist', e.message);
}


/**
 * Tests loading multiple modules by requesting a Deferred object.
 */
function testLoadMultiple() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setBatchModeEnabled(true);
  mm.setLoader(createSuccessfulBatchLoader(mm));

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;

  var dMap = mm.loadMultiple(['a', 'b']);
  dMap['a'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });

  assertFalse(calledBack);
  assertFalse(calledBack2);

  clock.tick(5);
  assertTrue(calledBack);
  assertFalse(calledBack2);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(2);

  assertTrue(calledBack);
  assertTrue(calledBack2);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));
  assertNull(error);
  assertNull(error2);
}


/**
 * Tests loading multiple modules with deps by requesting a Deferred object.
 */
function testLoadMultipleWithDeps() {
  var mm = getModuleManager({'a': [], 'b': ['c'], 'c': []});
  mm.setBatchModeEnabled(true);
  mm.setLoader(createSuccessfulBatchLoader(mm));

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;

  var dMap = mm.loadMultiple(['a', 'b']);
  dMap['a'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });

  assertFalse(calledBack);
  assertFalse(calledBack2);

  clock.tick(5);
  assertTrue(calledBack);
  assertFalse(calledBack2);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(2);

  assertFalse(calledBack2);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

  clock.tick(2);

  assertTrue(calledBack2);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
  assertNull(error);
  assertNull(error2);
}


/**
 * Tests loading multiple modules by requesting a Deferred object when
 * a server error occurs.
 */
function testLoadMultipleWithErrors() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setBatchModeEnabled(true);
  mm.setLoader(createUnsuccessfulLoader(mm, 500));

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;
  var calledBack3 = false;
  var error3 = null;

  var dMap = mm.loadMultiple(['a', 'b', 'c']);
  dMap['a'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });
  dMap['c'].then(
      function(ctx) {
        calledBack3 = true;
      },
      function(err) {
        error3 = err;
      });

  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);

  clock.tick(4);

  // A module request is now underway using the unsuccessful loader.
  // We substitute a successful loader for future module load requests.
  mm.setLoader(createSuccessfulBatchLoader(mm));

  clock.tick(1);

  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  // Retry should happen after a backoff
  clock.tick(5 + mm.getBackOff_());

  assertTrue(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(2);
  assertTrue(calledBack2);
  assertFalse(calledBack3);
  assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(2);
  assertTrue(calledBack3);
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

  assertNull(error);
  assertNull(error2);
  assertNull(error3);
}


/**
 * Tests loading multiple modules by requesting a Deferred object when
 * consecutive server error occur and the loader falls back to serial
 * loads.
 */
function testLoadMultipleWithErrorsFallbackOnSerial() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setBatchModeEnabled(true);
  mm.setLoader(createUnsuccessfulLoader(mm, 500));

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;
  var calledBack3 = false;
  var error3 = null;

  var dMap = mm.loadMultiple(['a', 'b', 'c']);
  dMap['a'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });
  dMap['c'].then(
      function(ctx) {
        calledBack3 = true;
      },
      function(err) {
        error3 = err;
      });

  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);

  clock.tick(5);

  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  // Retry should happen and fail after a backoff
  clock.tick(5 + mm.getBackOff_());
  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  // A second retry should happen after a backoff
  clock.tick(4 + mm.getBackOff_());
  // The second retry is now underway using the unsuccessful loader.
  // We substitute a successful loader for future module load requests.
  mm.setLoader(createSuccessfulBatchLoader(mm));

  clock.tick(1);

  // A second retry should fail now
  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  // Each module should be loaded individually now, each taking 5 ticks

  clock.tick(5);
  assertTrue(calledBack);
  assertFalse(calledBack2);
  assertFalse(calledBack3);
  assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
  assertFalse('module "b" should not be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(5);
  assertTrue(calledBack2);
  assertFalse(calledBack3);
  assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
  assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));

  clock.tick(5);
  assertTrue(calledBack3);
  assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));

  assertNull(error);
  assertNull(error2);
  assertNull(error3);
}


/**
 * Tests loading a module by user action by requesting a Deferred object.
 */
function testLoadForUser() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;
  var error = null;

  var d = mm.load('a', true);
  d.then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });

  assertFalse(calledBack);
  assertNull(error);
  assertTrue(mm.isUserActive());

  clock.tick(5);

  assertTrue(calledBack);
  assertNull(error);
}


/**
 * Tests that preloading a module calls back the deferred object.
 */
function testPreloadDeferredWhenNotLoaded() {
  var mm = getModuleManager({'a': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;

  var d = mm.preloadModule('a');
  d.addCallback(function(ctx) { calledBack = true; });

  // First load should take five ticks.
  assertFalse('module "a" should not be loaded yet', calledBack);
  clock.tick(5);
  assertTrue('module "a" should be loaded', calledBack);
}


/**
 * Tests preloading an already loaded module.
 */
function testPreloadDeferredWhenLoaded() {
  var mm = getModuleManager({'a': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;

  mm.preloadModule('a');
  clock.tick(5);

  var d = mm.preloadModule('a');
  d.addCallback(function(ctx) { calledBack = true; });

  // Module is already loaded, should be called back after the setTimeout
  // in preloadModule.
  assertFalse('deferred for module "a" should not be called yet', calledBack);
  clock.tick(1);
  assertTrue('module "a" should be loaded', calledBack);
}


/**
 * Tests preloading a module that is currently loading.
 */
function testPreloadDeferredWhenLoading() {
  var mm = getModuleManager({'a': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  mm.preloadModule('a');
  clock.tick(1);

  // 'b' is in the middle of loading, should get called back when it's done.
  var calledBack = false;
  var d = mm.preloadModule('a');
  d.addCallback(function(ctx) { calledBack = true; });

  assertFalse('module "a" should not be loaded yet', calledBack);
  clock.tick(4);
  assertTrue('module "a" should be loaded', calledBack);
}


/**
 * Tests that load doesn't trigger another load if a module is already
 * preloading.
 */
function testLoadWhenPreloading() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var origSetLoaded = mm.setLoaded;
  var calls = [0, 0];
  mm.beforeLoadModuleCode = function(id) { calls[0]++; };
  mm.setLoaded = function(id) {
    calls[1]++;
    origSetLoaded.call(mm, id);
  };

  var calledBack = false;
  var error = null;

  mm.preloadModule('c', 2);
  assertFalse('module "c" should not be loading yet', mm.isModuleLoading('c'));
  clock.tick(2);
  assertTrue('module "c" should now be loading', mm.isModuleLoading('c'));

  var d = mm.load('c');
  d.then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });

  assertTrue('module "c" should still be loading', mm.isModuleLoading('c'));
  clock.tick(5);
  assertFalse('module "c" should be done loading', mm.isModuleLoading('c'));
  assertEquals('beforeLoad should only be called once for "c"', 1, calls[0]);
  assertEquals('setLoaded should only be called once for "c"', 1, calls[1]);

  assertTrue(calledBack);
  assertNull(error);
}


/**
 * Tests that load doesn't trigger another load if a module is already
 * preloading.
 */
function testLoadMultipleWhenPreloading() {
  var mm = getModuleManager({'a': [], 'b': ['d'], 'c': [], 'd': []});
  mm.setLoader(createSuccessfulBatchLoader(mm));
  mm.setBatchModeEnabled(true);

  var origSetLoaded = mm.setLoaded;
  var calls = {'a': [0, 0], 'b': [0, 0], 'c': [0, 0], 'd': [0, 0]};
  mm.beforeLoadModuleCode = function(id) { calls[id][0]++; };
  mm.setLoaded = function(id) {
    calls[id][1]++;
    origSetLoaded.call(mm, id);
  };

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;
  var calledBack3 = false;
  var error3 = null;

  mm.preloadModule('c', 2);
  mm.preloadModule('d', 3);
  assertFalse('module "c" should not be loading yet', mm.isModuleLoading('c'));
  assertFalse('module "d" should not be loading yet', mm.isModuleLoading('d'));
  clock.tick(2);
  assertTrue('module "c" should now be loading', mm.isModuleLoading('c'));
  clock.tick(1);
  assertTrue('module "d" should now be loading', mm.isModuleLoading('d'));

  var dMap = mm.loadMultiple(['a', 'b', 'c']);
  dMap['a'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });
  dMap['c'].then(
      function(ctx) {
        calledBack3 = true;
      },
      function(err) {
        error3 = err;
      });

  assertTrue('module "a" should be loading', mm.isModuleLoading('a'));
  assertTrue('module "b" should be loading', mm.isModuleLoading('b'));
  assertTrue('module "c" should still be loading', mm.isModuleLoading('c'));
  clock.tick(4);
  assertTrue(calledBack3);

  assertFalse('module "c" should be done loading', mm.isModuleLoading('c'));
  assertTrue('module "d" should still be loading', mm.isModuleLoading('d'));
  clock.tick(5);
  assertFalse('module "d" should be done loading', mm.isModuleLoading('d'));

  assertFalse(calledBack);
  assertFalse(calledBack2);
  assertTrue('module "a" should still be loading', mm.isModuleLoading('a'));
  assertTrue('module "b" should still be loading', mm.isModuleLoading('b'));
  clock.tick(7);

  assertTrue(calledBack);
  assertTrue(calledBack2);
  assertFalse('module "a" should be done loading', mm.isModuleLoading('a'));
  assertFalse('module "b" should be done loading', mm.isModuleLoading('b'));

  assertEquals(
      'beforeLoad should only be called once for "a"', 1, calls['a'][0]);
  assertEquals(
      'setLoaded should only be called once for "a"', 1, calls['a'][1]);
  assertEquals(
      'beforeLoad should only be called once for "b"', 1, calls['b'][0]);
  assertEquals(
      'setLoaded should only be called once for "b"', 1, calls['b'][1]);
  assertEquals(
      'beforeLoad should only be called once for "c"', 1, calls['c'][0]);
  assertEquals(
      'setLoaded should only be called once for "c"', 1, calls['c'][1]);
  assertEquals(
      'beforeLoad should only be called once for "d"', 1, calls['d'][0]);
  assertEquals(
      'setLoaded should only be called once for "d"', 1, calls['d'][1]);

  assertNull(error);
  assertNull(error2);
  assertNull(error3);
}


/**
 * Tests that the deferred is still called when loadMultiple loads modules
 * that are already preloading.
 */
function testLoadMultipleWhenPreloadingSameModules() {
  var mm = getModuleManager({'a': [], 'b': ['d'], 'c': [], 'd': []});
  mm.setLoader(createSuccessfulBatchLoader(mm));
  mm.setBatchModeEnabled(true);

  var origSetLoaded = mm.setLoaded;
  var calls = {'c': [0, 0], 'd': [0, 0]};
  mm.beforeLoadModuleCode = function(id) { calls[id][0]++; };
  mm.setLoaded = function(id) {
    calls[id][1]++;
    origSetLoaded.call(mm, id);
  };

  var calledBack = false;
  var error = null;
  var calledBack2 = false;
  var error2 = null;

  mm.preloadModule('c', 2);
  mm.preloadModule('d', 3);
  assertFalse('module "c" should not be loading yet', mm.isModuleLoading('c'));
  assertFalse('module "d" should not be loading yet', mm.isModuleLoading('d'));
  clock.tick(2);
  assertTrue('module "c" should now be loading', mm.isModuleLoading('c'));
  clock.tick(1);
  assertTrue('module "d" should now be loading', mm.isModuleLoading('d'));

  var dMap = mm.loadMultiple(['c', 'd']);
  dMap['c'].then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });
  dMap['d'].then(
      function(ctx) {
        calledBack2 = true;
      },
      function(err) {
        error2 = err;
      });

  assertTrue('module "c" should still be loading', mm.isModuleLoading('c'));
  clock.tick(4);
  assertFalse('module "c" should be done loading', mm.isModuleLoading('c'));
  assertTrue('module "d" should still be loading', mm.isModuleLoading('d'));
  clock.tick(5);
  assertFalse('module "d" should be done loading', mm.isModuleLoading('d'));

  assertTrue(calledBack);
  assertTrue(calledBack2);

  assertEquals(
      'beforeLoad should only be called once for "c"', 1, calls['c'][0]);
  assertEquals(
      'setLoaded should only be called once for "c"', 1, calls['c'][1]);
  assertEquals(
      'beforeLoad should only be called once for "d"', 1, calls['d'][0]);
  assertEquals(
      'setLoaded should only be called once for "d"', 1, calls['d'][1]);

  assertNull(error);
  assertNull(error2);
}


/**
 * Tests loading a module via load when the module is already
 * loaded.  The deferred's callback should be called immediately.
 */
function testLoadWhenLoaded() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var calledBack = false;
  var error = null;

  mm.preloadModule('b', 2);
  clock.tick(10);

  assertFalse('module "b" should be done loading', mm.isModuleLoading('b'));

  var d = mm.load('b');
  d.then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });

  clock.tick(1);
  assertTrue(calledBack);
  assertNull(error);
}


/**
 * Tests that the deferred's errbacks are called if the module fails to load.
 */
function testLoadWithFailingModule() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createUnsuccessfulLoader(mm, 401));
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.UNAUTHORIZED, cause);
        firedLoadFailed = true;
      });
  var calledBack = false;
  var error = null;

  var d = mm.load('a');
  d.then(
      function(ctx) {
        calledBack = true;
      },
      function(err) {
        error = err;
      });

  assertFalse(calledBack);
  assertNull(error);

  clock.tick(500);

  assertFalse(calledBack);

  // NOTE: Deferred always calls errbacks with an Error object.  For now the
  // module manager just passes the FailureType which gets set as the Error
  // object's message.
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error.message));
}


/**
 * Tests that the deferred's errbacks are called if a module fails to load.
 */
function testLoadMultipleWithFailingModule() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(createUnsuccessfulLoader(mm, 401));
  mm.setBatchModeEnabled(true);
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.UNAUTHORIZED, cause);
      });
  var calledBack11 = false;
  var error11 = null;
  var calledBack12 = false;
  var error12 = null;
  var calledBack21 = false;
  var error21 = null;
  var calledBack22 = false;
  var error22 = null;

  var dMap = mm.loadMultiple(['a', 'b']);
  dMap['a'].then(
      function(ctx) {
        calledBack11 = true;
      },
      function(err) {
        error11 = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack12 = true;
      },
      function(err) {
        error12 = err;
      });

  var dMap2 = mm.loadMultiple(['b', 'c']);
  dMap2['b'].then(
      function(ctx) {
        calledBack21 = true;
      },
      function(err) {
        error21 = err;
      });
  dMap2['c'].then(
      function(ctx) {
        calledBack22 = true;
      },
      function(err) {
        error22 = err;
      });

  assertFalse(calledBack11);
  assertFalse(calledBack12);
  assertFalse(calledBack21);
  assertFalse(calledBack22);
  assertNull(error11);
  assertNull(error12);
  assertNull(error21);
  assertNull(error22);

  clock.tick(5);

  assertFalse(calledBack11);
  assertFalse(calledBack12);
  assertFalse(calledBack21);
  assertFalse(calledBack22);

  // NOTE: Deferred always calls errbacks with an Error object.  For now the
  // module manager just passes the FailureType which gets set as the Error
  // object's message.
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error11.message));
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error12.message));

  // The first deferred of the second load should be called since it asks for
  // one of the failed modules.
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error21.message));

  // The last deferred should be dropped so it is neither called back nor an
  // error.
  assertFalse(calledBack22);
  assertNull(error22);
}


/**
 * Tests that the right dependencies are cancelled on a loadMultiple failure.
 */
function testLoadMultipleWithFailingModuleDependencies() {
  var mm =
      getModuleManager({'a': [], 'b': [], 'c': ['b'], 'd': ['c'], 'e': []});
  mm.setLoader(createUnsuccessfulLoader(mm, 401));
  mm.setBatchModeEnabled(true);
  var cancelledIds = [];

  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.UNAUTHORIZED, cause);
        cancelledIds.push(id);
      });
  var calledBack11 = false;
  var error11 = null;
  var calledBack12 = false;
  var error12 = null;
  var calledBack21 = false;
  var error21 = null;
  var calledBack22 = false;
  var error22 = null;
  var calledBack23 = false;
  var error23 = null;

  var dMap = mm.loadMultiple(['a', 'b']);
  dMap['a'].then(
      function(ctx) {
        calledBack11 = true;
      },
      function(err) {
        error11 = err;
      });
  dMap['b'].then(
      function(ctx) {
        calledBack12 = true;
      },
      function(err) {
        error12 = err;
      });

  var dMap2 = mm.loadMultiple(['c', 'd', 'e']);
  dMap2['c'].then(
      function(ctx) {
        calledBack21 = true;
      },
      function(err) {
        error21 = err;
      });
  dMap2['d'].then(
      function(ctx) {
        calledBack22 = true;
      },
      function(err) {
        error22 = err;
      });
  dMap2['e'].then(
      function(ctx) {
        calledBack23 = true;
      },
      function(err) {
        error23 = err;
      });

  assertFalse(calledBack11);
  assertFalse(calledBack12);
  assertFalse(calledBack21);
  assertFalse(calledBack22);
  assertFalse(calledBack23);
  assertNull(error11);
  assertNull(error12);
  assertNull(error21);
  assertNull(error22);
  assertNull(error23);

  clock.tick(5);

  assertFalse(calledBack11);
  assertFalse(calledBack12);
  assertFalse(calledBack21);
  assertFalse(calledBack22);
  assertFalse(calledBack23);

  // NOTE: Deferred always calls errbacks with an Error object.  For now the
  // module manager just passes the FailureType which gets set as the Error
  // object's message.
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error11.message));
  assertEquals(
      'Failure cause was not as expected',
      goog.module.ModuleManager.FailureType.UNAUTHORIZED,
      Number(error12.message));

  // Check that among the failed modules, 'c' and 'd' are also cancelled
  // due to dependencies.
  assertTrue(goog.array.equals(['a', 'b', 'c', 'd'], cancelledIds.sort()));
}


/**
 * Tests that when loading multiple modules, the input array is not modified
 * when it has duplicates.
 */
function testLoadMultipleWithDuplicates() {
  var mm = getModuleManager({'a': [], 'b': []});
  mm.setBatchModeEnabled(true);
  mm.setLoader(createSuccessfulBatchLoader(mm));

  var listWithDuplicates = ['a', 'a', 'b'];
  mm.loadMultiple(listWithDuplicates);
  assertArrayEquals(
      'loadMultiple should not modify its input', ['a', 'a', 'b'],
      listWithDuplicates);
}


/**
 * Test loading dependencies transitively.
 */
function testLoadingDepsInNonBatchMode1() {
  var mm =
      getModuleManager({'i': [], 'j': [], 'k': ['j'], 'l': ['i', 'j', 'k']});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  mm.preloadModule('j');
  clock.tick(5);
  assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
  assertFalse('module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
  assertFalse('module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (1)', mm.isModuleLoaded('l'));

  // When loading a module in non-batch mode, its dependencies should be
  // requested independently, and in dependency order.
  mm.preloadModule('l');
  clock.tick(5);
  assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
  assertFalse('module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
  clock.tick(5);
  assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
  clock.tick(5);
  assertTrue('module "l" should be loaded', mm.isModuleLoaded('l'));
}


/**
 * Test loading dependencies transitively and in dependency order.
 */
function testLoadingDepsInNonBatchMode2() {
  var mm = getModuleManager({
    'h': [],
    'i': ['h'],
    'j': ['i'],
    'k': ['j'],
    'l': ['i', 'j', 'k'],
    'm': ['l']
  });
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  // When loading a module in non-batch mode, its dependencies should be
  // requested independently, and in dependency order. The order in this
  // case should be h,i,j,k,l,m.
  mm.preloadModule('m');
  clock.tick(5);
  assertTrue('module "h" should be loaded', mm.isModuleLoaded('h'));
  assertFalse('module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
  assertFalse('module "j" should not be loaded (1)', mm.isModuleLoaded('j'));
  assertFalse('module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (1)', mm.isModuleLoaded('l'));
  assertFalse('module "m" should not be loaded (1)', mm.isModuleLoaded('m'));

  clock.tick(5);
  assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
  assertFalse('module "j" should not be loaded (2)', mm.isModuleLoaded('j'));
  assertFalse('module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
  assertFalse('module "m" should not be loaded (2)', mm.isModuleLoaded('m'));

  clock.tick(5);
  assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
  assertFalse('module "k" should not be loaded (3)', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
  assertFalse('module "m" should not be loaded (3)', mm.isModuleLoaded('m'));

  clock.tick(5);
  assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
  assertFalse('module "l" should not be loaded (4)', mm.isModuleLoaded('l'));
  assertFalse('module "m" should not be loaded (4)', mm.isModuleLoaded('m'));

  clock.tick(5);
  assertTrue('module "l" should be loaded', mm.isModuleLoaded('l'));
  assertFalse('module "m" should not be loaded (5)', mm.isModuleLoaded('m'));

  clock.tick(5);
  assertTrue('module "m" should be loaded', mm.isModuleLoaded('m'));
}

function testLoadingDepsInBatchMode() {
  var mm =
      getModuleManager({'e': [], 'f': [], 'g': ['f'], 'h': ['e', 'f', 'g']});
  mm.setLoader(createSuccessfulBatchLoader(mm));
  mm.setBatchModeEnabled(true);

  mm.preloadModule('f');
  clock.tick(5);
  assertTrue('module "f" should be loaded', mm.isModuleLoaded('f'));
  assertFalse('module "e" should not be loaded (1)', mm.isModuleLoaded('e'));
  assertFalse('module "g" should not be loaded (1)', mm.isModuleLoaded('g'));
  assertFalse('module "h" should not be loaded (1)', mm.isModuleLoaded('h'));

  // When loading a module in batch mode, its not-yet-loaded dependencies
  // should be requested at the same time, and in dependency order.
  mm.preloadModule('h');
  clock.tick(5);
  assertTrue('module "e" should be loaded', mm.isModuleLoaded('e'));
  assertFalse('module "g" should not be loaded (2)', mm.isModuleLoaded('g'));
  assertFalse('module "h" should not be loaded (2)', mm.isModuleLoaded('h'));
  clock.tick(2);
  assertTrue('module "g" should be loaded', mm.isModuleLoaded('g'));
  assertFalse('module "h" should not be loaded (3)', mm.isModuleLoaded('h'));
  clock.tick(2);
  assertTrue('module "h" should be loaded', mm.isModuleLoaded('h'));
}


/**
 * Test unauthorized errors while loading modules.
 */
function testUnauthorizedLoading() {
  var mm = getModuleManager({'m': [], 'n': [], 'o': ['n']});
  mm.setLoader(createUnsuccessfulLoader(mm, 401));

  // Callback checks for an unauthorized error
  var firedLoadFailed = false;
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.UNAUTHORIZED, cause);
        firedLoadFailed = true;
      });
  mm.execOnLoad('o', function() {});
  assertTrue('module "o" should be loading', mm.isModuleLoading('o'));
  assertTrue('module "n" should be loading', mm.isModuleLoading('n'));
  clock.tick(5);
  assertTrue(
      'should have called unauthorized module callback', firedLoadFailed);
  assertFalse('module "o" should not be loaded', mm.isModuleLoaded('o'));
  assertFalse('module "o" should not be loading', mm.isModuleLoading('o'));
  assertFalse('module "n" should not be loaded', mm.isModuleLoaded('n'));
  assertFalse('module "n" should not be loading', mm.isModuleLoading('n'));
}


/**
 * Test error loading modules which are retried.
 */
function testErrorLoadingModule() {
  var mm = getModuleManager({'p': ['q'], 'q': [], 'r': ['q', 'p']});
  mm.setLoader(createUnsuccessfulLoader(mm, 500));

  mm.preloadModule('r');
  clock.tick(4);

  // A module request is now underway using the unsuccessful loader.
  // We substitute a successful loader for future module load requests.
  mm.setLoader(createSuccessfulNonBatchLoader(mm));
  clock.tick(1);
  assertFalse('module "q" should not be loaded (1)', mm.isModuleLoaded('q'));
  assertFalse('module "p" should not be loaded (1)', mm.isModuleLoaded('p'));
  assertFalse('module "r" should not be loaded (1)', mm.isModuleLoaded('r'));

  // Failed loads are automatically retried after a backOff.
  clock.tick(5 + mm.getBackOff_());
  assertTrue('module "q" should be loaded', mm.isModuleLoaded('q'));
  assertFalse('module "p" should not be loaded (2)', mm.isModuleLoaded('p'));
  assertFalse('module "r" should not be loaded (2)', mm.isModuleLoaded('r'));

  // A successful load decrements the backOff.
  clock.tick(5);
  assertTrue('module "p" should be loaded', mm.isModuleLoaded('p'));
  assertFalse('module "r" should not be loaded (3)', mm.isModuleLoaded('r'));
  clock.tick(5);
  assertTrue('module "r" should be loaded', mm.isModuleLoaded('r'));
}


/**
 * Tests error loading modules which are retried.
 */
function testErrorLoadingModule_batchMode() {
  var mm = getModuleManager({'p': ['q'], 'q': [], 'r': ['q', 'p']});
  mm.setLoader(createUnsuccessfulBatchLoader(mm, 500));
  mm.setBatchModeEnabled(true);

  mm.preloadModule('r');
  clock.tick(4);

  // A module request is now underway using the unsuccessful loader.
  // We substitute a successful loader for future module load requests.
  mm.setLoader(createSuccessfulBatchLoader(mm));
  clock.tick(1);
  assertFalse('module "q" should not be loaded (1)', mm.isModuleLoaded('q'));
  assertFalse('module "p" should not be loaded (1)', mm.isModuleLoaded('p'));
  assertFalse('module "r" should not be loaded (1)', mm.isModuleLoaded('r'));

  // Failed loads are automatically retried after a backOff.
  clock.tick(5 + mm.getBackOff_());
  assertTrue('module "q" should be loaded', mm.isModuleLoaded('q'));
  clock.tick(2);
  assertTrue('module "p" should not be loaded (2)', mm.isModuleLoaded('p'));
  clock.tick(2);
  assertTrue('module "r" should not be loaded (2)', mm.isModuleLoaded('r'));
}


/**
 * Test consecutive errors in loading modules.
 */
function testConsecutiveErrors() {
  var mm = getModuleManager({'s': []});
  mm.setLoader(createUnsuccessfulLoader(mm, 500));

  // Register an error callback for consecutive failures.
  var firedLoadFailed = false;
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.CONSECUTIVE_FAILURES, cause);
        firedLoadFailed = true;
      });

  mm.preloadModule('s');
  assertFalse('module "s" should not be loaded (0)', mm.isModuleLoaded('s'));

  // Fail twice.
  for (var i = 0; i < 2; i++) {
    clock.tick(5 + mm.getBackOff_());
    assertFalse('module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
    assertFalse('should not fire failed callback (1)', firedLoadFailed);
  }

  // Fail a third time and check that the callback is fired.
  clock.tick(5 + mm.getBackOff_());
  assertFalse('module "s" should not be loaded (2)', mm.isModuleLoaded('s'));
  assertTrue('should have fired failed callback', firedLoadFailed);

  // Check that it doesn't attempt to load the module anymore after it has
  // failed.
  var triedLoad = false;
  mm.setLoader({
    loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn) {
      triedLoad = true;
    }
  });

  // Also reset the failed callback flag and make sure it isn't called
  // again.
  firedLoadFailed = false;
  clock.tick(10 + mm.getBackOff_());
  assertFalse('module "s" should not be loaded (3)', mm.isModuleLoaded('s'));
  assertFalse('No more loads should have been tried', triedLoad);
  assertFalse(
      'The load failed callback should be fired only once', firedLoadFailed);
}


/**
 * Test loading errors due to old code.
 */
function testOldCodeGoneError() {
  var mm = getModuleManager({'s': []});
  mm.setLoader(createUnsuccessfulLoader(mm, 410));

  // Callback checks for an old code failure
  var firedLoadFailed = false;
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.OLD_CODE_GONE, cause);
        firedLoadFailed = true;
      });

  mm.preloadModule('s', 0);
  assertFalse('module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
  clock.tick(5);
  assertFalse('module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
  assertTrue('should have called old code gone callback', firedLoadFailed);
}


/**
 * Test timeout.
 */
function testTimeout() {
  var mm = getModuleManager({'s': []});
  mm.setLoader(createTimeoutLoader(mm));

  // Callback checks for timeout
  var firedTimeout = false;
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR,
      function(callbackType, id, cause) {
        assertEquals(
            'Failure cause was not as expected',
            goog.module.ModuleManager.FailureType.TIMEOUT, cause);
        firedTimeout = true;
      });

  mm.preloadModule('s', 0);
  assertFalse('module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
  clock.tick(5);
  assertFalse('module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
  assertTrue('should have called timeout callback', firedTimeout);
}


/**
 * Tests that an error during execOnLoad will trigger the error callback.
 */
function testExecOnLoadError() {
  // Expect two callbacks, each of which will be called with callback type
  // ERROR, the right module id and failure type INIT_ERROR.
  var errorCallback1 = goog.testing.createFunctionMock('callback1');
  errorCallback1(
      goog.module.ModuleManager.CallbackType.ERROR, 'b',
      goog.module.ModuleManager.FailureType.INIT_ERROR);

  var errorCallback2 = goog.testing.createFunctionMock('callback2');
  errorCallback2(
      goog.module.ModuleManager.CallbackType.ERROR, 'b',
      goog.module.ModuleManager.FailureType.INIT_ERROR);

  errorCallback1.$replay();
  errorCallback2.$replay();

  var mm = new goog.module.ModuleManager();
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  // Register the first callback before setting the module info map.
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR, errorCallback1);

  mm.setAllModuleInfo({'a': [], 'b': [], 'c': []});

  // Register the second callback after setting the module info map.
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR, errorCallback2);

  var execOnLoadBCalled = false;
  mm.execOnLoad('b', function() {
    execOnLoadBCalled = true;
    throw new Error();
  });

  assertThrows(function() { clock.tick(5); });

  assertTrue(
      'execOnLoad should have been called on module b.', execOnLoadBCalled);
  errorCallback1.$verify();
  errorCallback2.$verify();
}


/**
 * Tests that an error during execOnLoad will trigger the error callback.
 * Uses setAllModuleInfoString rather than setAllModuleInfo.
 */
function testExecOnLoadErrorModuleInfoString() {
  // Expect a callback to be called with callback type ERROR, the right module
  // id and failure type INIT_ERROR.
  var errorCallback = goog.testing.createFunctionMock('callback');
  errorCallback(
      goog.module.ModuleManager.CallbackType.ERROR, 'b',
      goog.module.ModuleManager.FailureType.INIT_ERROR);

  errorCallback.$replay();

  var mm = new goog.module.ModuleManager();
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  // Register the first callback before setting the module info map.
  mm.registerCallback(
      goog.module.ModuleManager.CallbackType.ERROR, errorCallback);

  mm.setAllModuleInfoString('a/b/c');

  var execOnLoadBCalled = false;
  mm.execOnLoad('b', function() {
    execOnLoadBCalled = true;
    throw new Error();
  });

  assertThrows(function() { clock.tick(5); });

  assertTrue(
      'execOnLoad should have been called on module b.', execOnLoadBCalled);
  errorCallback.$verify();
}


/**
 * Make sure ModuleInfo objects in moduleInfoMap_ get disposed.
 */
function testDispose() {
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});

  var moduleInfoA = mm.getModuleInfo('a');
  assertNotNull(moduleInfoA);
  var moduleInfoB = mm.getModuleInfo('b');
  assertNotNull(moduleInfoB);
  var moduleInfoC = mm.getModuleInfo('c');
  assertNotNull(moduleInfoC);

  mm.dispose();
  assertTrue(moduleInfoA.isDisposed());
  assertTrue(moduleInfoB.isDisposed());
  assertTrue(moduleInfoC.isDisposed());
}

function testDependencyOrderingWithSimpleDeps() {
  var mm = getModuleManager({
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['e', 'f'],
    'd': [],
    'e': [],
    'f': []
  });
  var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['d', 'e', 'f', 'b', 'c', 'a'], ids);
}

function testDependencyOrderingWithRequestedDep() {
  var mm = getModuleManager({
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['e', 'f'],
    'd': [],
    'e': [],
    'f': []
  });
  mm.requestedModuleIds_ = ['a', 'b'];
  var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['e', 'f', 'c'], ids);
}

function testDependencyOrderingWithCommonDepsInDeps() {
  // Tests to make sure that if dependencies of the root are loaded before
  // their common dependencies.
  var mm = getModuleManager({'a': ['b', 'c'], 'b': ['d'], 'c': ['d'], 'd': []});
  var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['d', 'b', 'c', 'a'], ids);
}

function testDependencyOrderingWithCommonDepsInRoot1() {
  // Tests the case where a dependency of the root depends on another
  // dependency of the root.  Regardless of ordering in the root's
  // deps.
  var mm = getModuleManager({'a': ['b', 'c'], 'b': ['c'], 'c': []});
  var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['c', 'b', 'a'], ids);
}

function testDependencyOrderingWithCommonDepsInRoot2() {
  // Tests the case where a dependency of the root depends on another
  // dependency of the root.  Regardless of ordering in the root's
  // deps.
  var mm = getModuleManager({'a': ['b', 'c'], 'b': [], 'c': ['b']});
  var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['b', 'c', 'a'], ids);
}

function testDependencyOrderingWithGmailExample() {
  // Real dependency graph taken from gmail.
  var mm = getModuleManager({
    's': ['dp', 'ml', 'md'],
    'dp': ['a'],
    'ml': ['ld', 'm'],
    'ld': ['a'],
    'm': ['ad', 'mh', 'n'],
    'md': ['mh', 'ld'],
    'a': [],
    'mh': [],
    'ad': [],
    'n': []
  });

  mm.setLoaded('a');
  mm.setLoaded('m');
  mm.setLoaded('n');
  mm.setLoaded('ad');
  mm.setLoaded('mh');

  var ids = mm.getNotYetLoadedTransitiveDepIds_('s');
  assertDependencyOrder(ids, mm);
  assertArrayEquals(['ld', 'dp', 'ml', 'md', 's'], ids);
}

function assertDependencyOrder(list, mm) {
  var seen = {};
  for (var i = 0; i < list.length; i++) {
    var id = list[i];
    seen[id] = true;
    var deps = mm.getModuleInfo(id).getDependencies();
    for (var j = 0; j < deps.length; j++) {
      var dep = deps[j];
      assertTrue(
          'Unresolved dependency [' + dep + '] for [' + id + '].',
          seen[dep] || mm.getModuleInfo(dep).isLoaded());
    }
  }
}

function testRegisterInitializationCallback() {
  var initCalled = 0;
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  mm.setLoader(
      createSuccessfulNonBatchLoaderWithRegisterInitCallback(
          mm, function() { ++initCalled; }));
  execOnLoad_(mm);
  // execOnLoad_ loads modules a and c
  assertTrue(initCalled == 2);
}

function createSuccessfulNonBatchLoaderWithRegisterInitCallback(moduleMgr, fn) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      moduleMgr.beforeLoadModuleCode(ids[0]);
      moduleMgr.registerInitializationCallback(fn);
      setTimeout(function() {
        moduleMgr.setLoaded(ids[0]);
        if (opt_successFn) {
          opt_successFn();
        }
      }, 5);
    }
  };
}

function testSetModuleConstructor() {
  var initCalled = 0;
  var mm = getModuleManager({'a': [], 'b': [], 'c': []});
  var info = {
    'a': {ctor: AModule, count: 0},
    'b': {ctor: BModule, count: 0},
    'c': {ctor: CModule, count: 0}
  };
  function AModule() {
    ++info['a'].count;
    goog.module.BaseModule.call(this);
  }
  goog.inherits(AModule, goog.module.BaseModule);
  function BModule() {
    ++info['b'].count;
    goog.module.BaseModule.call(this);
  }
  goog.inherits(BModule, goog.module.BaseModule);
  function CModule() {
    ++info['c'].count;
    goog.module.BaseModule.call(this);
  }
  goog.inherits(CModule, goog.module.BaseModule);

  mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
  execOnLoad_(mm);
  assertTrue(info['a'].count == 1);
  assertTrue(info['b'].count == 0);
  assertTrue(info['c'].count == 1);
  assertTrue(mm.getModuleInfo('a').getModule() instanceof AModule);
  assertTrue(mm.getModuleInfo('c').getModule() instanceof CModule);
}


/**
 * Tests that a call to load the loading module during module initialization
 * doesn't trigger a second load.
 */
function testLoadWhenInitializing() {
  var mm = getModuleManager({'a': []});
  mm.setLoader(createSuccessfulNonBatchLoader(mm));

  var info = {'a': {ctor: AModule, count: 0}};
  function AModule() {
    ++info['a'].count;
    goog.module.BaseModule.call(this);
  }
  goog.inherits(AModule, goog.module.BaseModule);
  AModule.prototype.initialize = function() { mm.load('a'); };
  mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
  mm.preloadModule('a');
  clock.tick(5);
  assertEquals(info['a'].count, 1);
}

function testErrorInEarlyCallback() {
  var errback = goog.testing.recordFunction();
  var callback = goog.testing.recordFunction();
  var mm = getModuleManager({'a': [], 'b': ['a']});
  mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
  mm.getModuleInfo('a').registerCallback(callback);
  mm.getModuleInfo('a').registerErrback(errback);

  mm.setLoader(
      createSuccessfulNonBatchLoaderWithConstructor(
          mm, createModulesFor('a', 'b')));
  mm.preloadModule('b');
  var e = assertThrows(function() { clock.tick(5); });

  assertEquals('error', e.message);
  assertEquals(0, callback.getCallCount());
  assertEquals(1, errback.getCallCount());
  assertEquals(
      goog.module.ModuleManager.FailureType.INIT_ERROR,
      errback.getLastCall().getArguments()[0]);
  assertTrue(mm.getModuleInfo('a').isLoaded());
  assertFalse(mm.getModuleInfo('b').isLoaded());

  clock.tick(5);
  assertTrue(mm.getModuleInfo('b').isLoaded());
}

function testErrorInNormalCallback() {
  var earlyCallback = goog.testing.recordFunction();
  var errback = goog.testing.recordFunction();
  var mm = getModuleManager({'a': [], 'b': ['a']});
  mm.getModuleInfo('a').registerEarlyCallback(earlyCallback);
  mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
  mm.getModuleInfo('a').registerErrback(errback);

  mm.setLoader(
      createSuccessfulNonBatchLoaderWithConstructor(
          mm, createModulesFor('a', 'b')));
  mm.preloadModule('b');
  var e = assertThrows(function() { clock.tick(10); });
  clock.tick(10);

  assertEquals('error', e.message);
  assertEquals(1, errback.getCallCount());
  assertEquals(
      goog.module.ModuleManager.FailureType.INIT_ERROR,
      errback.getLastCall().getArguments()[0]);
  assertTrue(mm.getModuleInfo('a').isLoaded());
  assertTrue(mm.getModuleInfo('b').isLoaded());
}

function testErrorInErrback() {
  var mm = getModuleManager({'a': [], 'b': ['a']});
  mm.getModuleInfo('a').registerCallback(goog.functions.error('error1'));
  mm.getModuleInfo('a').registerErrback(goog.functions.error('error2'));

  mm.setLoader(
      createSuccessfulNonBatchLoaderWithConstructor(
          mm, createModulesFor('a', 'b')));
  mm.preloadModule('a');
  var e = assertThrows(function() { clock.tick(10); });
  assertEquals('error1', e.message);
  var e = assertThrows(function() { clock.tick(10); });
  assertEquals('error2', e.message);
  assertTrue(mm.getModuleInfo('a').isLoaded());
}

function createModulesFor(var_args) {
  var result = {};
  for (var i = 0; i < arguments.length; i++) {
    var key = arguments[i];
    result[key] = {ctor: goog.module.BaseModule};
  }
  return result;
}

function createSuccessfulNonBatchLoaderWithConstructor(moduleMgr, info) {
  return {
    loadModules: function(
        ids, moduleInfoMap, opt_successFn, opt_errFn, opt_timeoutFn) {
      setTimeout(function() {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        moduleMgr.setModuleConstructor(info[ids[0]].ctor);
        moduleMgr.setLoaded(ids[0]);
        if (opt_successFn) {
          opt_successFn();
        }
      }, 5);
    }
  };
}

function testInitCallbackInBaseModule() {
  var mm = new goog.module.ModuleManager();
  var called = false;
  var context;
  mm.registerInitializationCallback(function(mcontext) {
    called = true;
    context = mcontext;
  });
  mm.setAllModuleInfo({'a': [], 'b': ['a']});
  assertTrue('Base initialization not called', called);
  assertNull('Context should still be null', context);

  var mm = new goog.module.ModuleManager();
  called = false;
  mm.registerInitializationCallback(function(mcontext) {
    called = true;
    context = mcontext;
  });
  var appContext = {};
  mm.setModuleContext(appContext);
  assertTrue('Base initialization not called after setModuleContext', called);
  assertEquals('Did not receive module context', appContext, context);
}

function testSetAllModuleInfoString() {
  var info = 'base/one:0/two:0/three:0,1,2/four:0,3/five:';
  var mm = new goog.module.ModuleManager();
  mm.setAllModuleInfoString(info);

  assertNotNull('Base should exist', mm.getModuleInfo('base'));
  assertNotNull('One should exist', mm.getModuleInfo('one'));
  assertNotNull('Two should exist', mm.getModuleInfo('two'));
  assertNotNull('Three should exist', mm.getModuleInfo('three'));
  assertNotNull('Four should exist', mm.getModuleInfo('four'));
  assertNotNull('Five should exist', mm.getModuleInfo('five'));

  assertArrayEquals(
      ['base', 'one', 'two'], mm.getModuleInfo('three').getDependencies());
  assertArrayEquals(
      ['base', 'three'], mm.getModuleInfo('four').getDependencies());
  assertArrayEquals([], mm.getModuleInfo('five').getDependencies());
}

function testSetAllModuleInfoStringWithEmptyString() {
  var mm = new goog.module.ModuleManager();
  var called = false;
  var context;
  mm.registerInitializationCallback(function(mcontext) {
    called = true;
    context = mcontext;
  });
  mm.setAllModuleInfoString('');
  assertTrue('Initialization not called', called);
}

function testBackOffAmounts() {
  var mm = new goog.module.ModuleManager();
  assertEquals(0, mm.getBackOff_());

  mm.consecutiveFailures_++;
  assertEquals(5000, mm.getBackOff_());

  mm.consecutiveFailures_++;
  assertEquals(20000, mm.getBackOff_());
}


/**
 * Tests that the IDLE callbacks are executed for active->idle transitions
 * after setAllModuleInfoString with currently loading modules.
 */
function testIdleCallbackWithInitialModules() {
  var callback = goog.testing.recordFunction();

  var mm = new goog.module.ModuleManager();
  mm.setAllModuleInfoString('a', ['a']);
  mm.registerCallback(goog.module.ModuleManager.CallbackType.IDLE, callback);

  assertTrue(mm.isActive());

  mm.beforeLoadModuleCode('a');

  assertEquals(0, callback.getCallCount());

  mm.setLoaded('a');

  assertFalse(mm.isActive());

  assertEquals(1, callback.getCallCount());
}
