// Copyright 2006 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.net.ImageLoaderTest');
goog.setTestOnly('goog.net.ImageLoaderTest');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dispose');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.net.ImageLoader');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');

var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(document.title);

// Set the AsyncTestCase timeout to larger value to allow more time
// for images to load.
asyncTestCase.stepTimeout = 5000;


var TEST_EVENT_TYPES = [
  goog.events.EventType.LOAD,
  goog.net.EventType.COMPLETE,
  goog.net.EventType.ERROR
];


/**
 * Mapping from test image file name to:
 * [expected width, expected height, expected event to be fired].
 */
var TEST_IMAGES = {
  'imageloader_testimg1.gif': [20, 20, goog.events.EventType.LOAD],
  'imageloader_testimg2.gif': [20, 20, goog.events.EventType.LOAD],
  'imageloader_testimg3.gif': [32, 32, goog.events.EventType.LOAD],

  'this-is-not-image-1.gif': [0, 0, goog.net.EventType.ERROR],
  'this-is-not-image-2.gif': [0, 0, goog.net.EventType.ERROR]
};


var startTime;
var loader;


function setUp() {
  startTime = goog.now();

  loader = new goog.net.ImageLoader();

  // Adds test images to the loader.
  var i = 0;
  for (var key in TEST_IMAGES) {
    var imageId = 'img_' + i++;
    loader.addImage(imageId, key);
  }
}


function tearDown() {
  goog.dispose(loader);
}


/**
 * Tests loading image and disposing before loading completes.
 */
function testDisposeInTheMiddleOfLoadingWorks() {
  goog.events.listen(loader, TEST_EVENT_TYPES,
      goog.partial(handleDisposalImageLoaderEvent, loader));
  // waitForAsync before starting loader just in case
  // handleDisposalImageLoaderEvent is called from within loader.start
  // (before we yield control). This may happen in IE7/IE8.
  asyncTestCase.waitForAsync('Waiting for loader handler to fire.');
  loader.start();
}


function handleDisposalImageLoaderEvent(loader, e) {
  assertFalse('Handler is still invoked after loader is disposed.',
      loader.isDisposed());

  switch (e.type) {
    case goog.net.EventType.COMPLETE:
      fail('This test should never get COMPLETE event.');
      return;

    case goog.events.EventType.LOAD:
    case goog.net.EventType.ERROR:
      loader.dispose();
      break;
  }

  // Make sure that handler is never called again after disposal before
  // marking test as successful.
  asyncTestCase.waitForAsync('Wait to ensure that COMPLETE is never fired');
  goog.Timer.callOnce(function() {
    asyncTestCase.continueTesting();
  }, 500);
}


/**
 * Tests loading of images until completion.
 */
function testLoadingUntilCompletion() {
  var results = {};
  goog.events.listen(loader, TEST_EVENT_TYPES,
      function(e) {
        switch (e.type) {
          case goog.events.EventType.LOAD:
            var image = e.target;
            results[image.src.substring(image.src.lastIndexOf('/') + 1)] =
                [image.naturalWidth, image.naturalHeight, e.type];
            return;

          case goog.net.EventType.ERROR:
            var image = e.target;
            results[image.src.substring(image.src.lastIndexOf('/') + 1)] =
                [image.naturalWidth, image.naturalHeight, e.type];
            return;

          case goog.net.EventType.COMPLETE:
            // Test completes successfully.
            asyncTestCase.continueTesting();

            assertImagesAreCorrect(results);
            return;
        }
      });

  // waitForAsync before starting loader just in case handleImageLoaderEvent
  // is called from within loader.start (before we yield control).
  // This may happen in IE7/IE8.
  asyncTestCase.waitForAsync('Waiting for loader handler to fire.');
  loader.start();
}


function assertImagesAreCorrect(results) {
  assertEquals(
      goog.object.getCount(TEST_IMAGES), goog.object.getCount(results));
  goog.object.forEach(TEST_IMAGES, function(value, key) {
    // Check if fires the COMPLETE event.
    assertTrue('Image is not loaded completely.', key in results);

    var image = results[key];

    // Check image size.
    assertEquals('Image width is not correct', value[0], image[0]);
    assertEquals('Image length is not correct', value[1], image[1]);

    // Check if fired the correct event.
    assertEquals('Event *' + value[2] + '* must be fired', value[2], image[2]);
  });
}


/**
 * Overrides the loader's loadImage_ method so that it dispatches an image
 * loaded event immediately, causing any event listners to receive them
 * synchronously.  This allows tests to assume synchronous execution.
 */
function makeLoaderSynchronous(loader) {
  var originalLoadImage = loader.loadImage_;
  loader.loadImage_ = function(request, id) {
    originalLoadImage.call(this, request, id);

    var event = new goog.events.Event(goog.events.EventType.LOAD);
    event.currentTarget = this.imageIdToImageMap_[id];
    loader.onNetworkEvent_(event);
  };

  // Make listen() a no-op.
  loader.handler_.listen = goog.nullFunction;
}


/**
 * Verifies that if an additional image is added after start() was called, but
 * before COMPLETE was dispatched, no COMPLETE event is sent.  Verifies COMPLETE
 * is finally sent when .start() is called again and all images have now
 * completed loading.
 */
function testImagesAddedAfterStart() {
  // Use synchronous image loading.
  makeLoaderSynchronous(loader);

  // Add another image once the first images finishes loading.
  goog.events.listenOnce(loader, goog.events.EventType.LOAD, function() {
    loader.addImage('extra_image', 'extra_image.gif');
  });

  // Keep track of the total # of image loads.
  var loadRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);

  // Keep track of how many times COMPLETE was dispatched.
  var completeRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);

  // Start testing.
  loader.start();
  assertEquals(
      'COMPLETE event should not have been dispatched yet: An image was ' +
          'added after the initial batch was started.',
      0, completeRecordFn.getCallCount());
  assertEquals('Just the test images should have loaded',
      goog.object.getCount(TEST_IMAGES), loadRecordFn.getCallCount());

  loader.start();
  assertEquals('COMPLETE should have been dispatched once.',
      1, completeRecordFn.getCallCount());
  assertEquals('All images should have been loaded',
      goog.object.getCount(TEST_IMAGES) + 1, loadRecordFn.getCallCount());
}


/**
 * Verifies that more images can be added after an upload starts, and start()
 * can be called for them, resulting in just one COMPLETE event once all the
 * images have completed.
 */
function testImagesAddedAndStartedAfterStart() {
  // Use synchronous image loading.
  makeLoaderSynchronous(loader);

  // Keep track of the total # of image loads.
  var loadRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);

  // Add more images once the first images finishes loading, and call start()
  // to get them going.
  goog.events.listenOnce(loader, goog.events.EventType.LOAD, function(e) {
    loader.addImage('extra_image', 'extra_image.gif');
    loader.addImage('extra_image2', 'extra_image2.gif');
    loader.start();
  });

  // Keep track of how many times COMPLETE was dispatched.
  var completeRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);

  // Start testing.  Make sure all 7 images loaded.
  loader.start();
  assertEquals('COMPLETE should have been dispatched once.',
      1, completeRecordFn.getCallCount());
  assertEquals('All images should have been loaded',
      goog.object.getCount(TEST_IMAGES) + 2, loadRecordFn.getCallCount());
}


/**
 * Verifies that if images are removed after loading has started, COMPLETE
 * is dispatched once the remaining images have finished.
 */
function testImagesRemovedAfterStart() {
  // Use synchronous image loading.
  makeLoaderSynchronous(loader);

  // Remove 2 images once the first image finishes loading.
  goog.events.listenOnce(loader, goog.events.EventType.LOAD, function(e) {
    loader.removeImage(
        goog.array.peek(goog.object.getKeys(this.imageIdToRequestMap_)));
    loader.removeImage(
        goog.array.peek(goog.object.getKeys(this.imageIdToRequestMap_)));
  });

  // Keep track of the total # of image loads.
  var loadRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);

  // Keep track of how many times COMPLETE was dispatched.
  var completeRecordFn = goog.testing.recordFunction();
  goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);

  // Start testing.  Make sure only the 3 images remaining loaded.
  loader.start();
  assertEquals('COMPLETE should have been dispatched once.',
      1, completeRecordFn.getCallCount());
  assertEquals('All images should have been loaded',
      goog.object.getCount(TEST_IMAGES) - 2, loadRecordFn.getCallCount());
}


/**
 * Verifies that the correct image attribute is set when using CORS requests.
 */
function testSetsCorsAttribute() {
  // Use synchronous image loading.
  makeLoaderSynchronous(loader);

  // Verify the crossOrigin attribute of the requested images.
  goog.events.listen(loader, goog.events.EventType.LOAD, function(e) {
    var image = e.target;
    if (image.id == 'cors_request') {
      assertEquals(
          'CORS requested image should have a crossOrigin attribute set',
          'anonymous', image.crossOrigin);
    } else {
      assertTrue(
          'Non-CORS requested images should not have a crossOrigin attribute',
          goog.string.isEmptyOrWhitespace(goog.string.makeSafe(image.crossOrigin)));
    }
  });

  // Make a new request for one of the images, this time using CORS.
  var srcs = goog.object.getKeys(TEST_IMAGES);
  loader.addImage(
      'cors_request', srcs[0], goog.net.ImageLoader.CorsRequestType.ANONYMOUS);
  loader.start();
}
