// Copyright 2011 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.crypt.BlobHasherTest');
goog.setTestOnly('goog.crypt.BlobHasherTest');

goog.require('goog.crypt');
goog.require('goog.crypt.BlobHasher');
goog.require('goog.crypt.Md5');
goog.require('goog.events');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');

// A browser-independent mock of goog.fs.sliceBlob. The actual implementation
// calls the underlying slice method differently based on browser version.
// This mock does not support negative opt_end.
var fsSliceBlobMock = function(blob, start, opt_end) {
  if (!goog.isNumber(opt_end)) {
    opt_end = blob.size;
  }
  return blob.slice(start, opt_end);
};

// Mock out the Blob using a string.
BlobMock = function(string) {
  this.data = string;
  this.size = this.data.length;
};

BlobMock.prototype.slice = function(start, end) {
  return new BlobMock(this.data.substr(start, end - start));
};


// Mock out the FileReader to have control over the flow.
FileReaderMock = function() {
  this.array_ = [];
  this.result = null;
  this.readyState = this.EMPTY;

  this.onload = null;
  this.onabort = null;
  this.onerror = null;
};

FileReaderMock.prototype.EMPTY = 0;
FileReaderMock.prototype.LOADING = 1;
FileReaderMock.prototype.DONE = 2;

FileReaderMock.prototype.mockLoad = function() {
  this.readyState = this.DONE;
  this.result = this.array_;
  if (this.onload) {
    this.onload.call();
  }
};

FileReaderMock.prototype.abort = function() {
  this.readyState = this.DONE;
  if (this.onabort) {
    this.onabort.call();
  }
};

FileReaderMock.prototype.mockError = function() {
  this.readyState = this.DONE;
  if (this.onerror) {
    this.onerror.call();
  }
};

FileReaderMock.prototype.readAsArrayBuffer = function(blobMock) {
  this.readyState = this.LOADING;
  this.array_ = [];
  for (var i = 0; i < blobMock.size; ++i) {
    this.array_[i] = blobMock.data.charCodeAt(i);
  }
};

FileReaderMock.prototype.isLoading = function() {
  return this.readyState == this.LOADING;
};

var stubs = new goog.testing.PropertyReplacer();
function setUp() {
  stubs.set(goog.global, 'FileReader', FileReaderMock);
  stubs.set(goog.fs, 'sliceBlob', fsSliceBlobMock);
}

function tearDown() {
  stubs.reset();
}


/**
 * Makes the blobHasher read chunks from the blob and hash it. The number of
 * reads shall not exceed a pre-determined number (typically blob size / chunk
 * size) for computing hash. This function fails fast (after maxReads is
 * reached), assuming that the hasher failed to generate hashes. This prevents
 * the test suite from going into infinite loop.
 * @param {!goog.crypt.BlobHasher} blobHasher Hasher in action.
 * @param {number} maxReads Max number of read attempts.
 */
function readFromBlob(blobHasher, maxReads) {
  var counter = 0;
  while (blobHasher.fileReader_ && blobHasher.fileReader_.isLoading() &&
         counter <= maxReads) {
    blobHasher.fileReader_.mockLoad();
    counter++;
  }
  assertTrue(counter <= maxReads);
  return counter;
}

function testBasicOperations() {
  if (!window.Blob) {
    return;
  }

  // Test hashing with one chunk.
  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn);
  var blob = new BlobMock('The quick brown fox jumps over the lazy dog');
  blobHasher.hash(blob);
  readFromBlob(blobHasher, 1);
  assertEquals(
      '9e107d9d372bb6826bd81d3542a419d6',
      goog.crypt.byteArrayToHex(blobHasher.getHash()));

  // Test hashing with multiple chunks.
  blobHasher = new goog.crypt.BlobHasher(hashFn, 7);
  blobHasher.hash(blob);
  readFromBlob(blobHasher, Math.ceil(blob.size / 7));
  assertEquals(
      '9e107d9d372bb6826bd81d3542a419d6',
      goog.crypt.byteArrayToHex(blobHasher.getHash()));

  // Test hashing with no chunks.
  blob = new BlobMock('');
  blobHasher.hash(blob);
  readFromBlob(blobHasher, 1);
  assertEquals(
      'd41d8cd98f00b204e9800998ecf8427e',
      goog.crypt.byteArrayToHex(blobHasher.getHash()));
}

function testNormalFlow() {
  if (!window.Blob) {
    return;
  }

  // Test the flow with one chunk.
  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn, 13);
  var blob = new BlobMock('short');
  var startedEvents = 0;
  var progressEvents = 0;
  var completeEvents = 0;
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.STARTED,
      function() { ++startedEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.PROGRESS,
      function() { ++progressEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE,
      function() { ++completeEvents; });
  blobHasher.hash(blob);
  assertEquals(1, startedEvents);
  assertEquals(0, progressEvents);
  assertEquals(0, completeEvents);
  readFromBlob(blobHasher, 1);
  assertEquals(1, startedEvents);
  assertEquals(1, progressEvents);
  assertEquals(1, completeEvents);

  // Test the flow with multiple chunks.
  blob = new BlobMock('The quick brown fox jumps over the lazy dog');
  startedEvents = 0;
  progressEvents = 0;
  completeEvents = 0;
  var progressLoops = 0;
  blobHasher.hash(blob);
  assertEquals(1, startedEvents);
  assertEquals(0, progressEvents);
  assertEquals(0, completeEvents);
  progressLoops = readFromBlob(blobHasher, Math.ceil(blob.size / 13));
  assertEquals(1, startedEvents);
  assertEquals(progressLoops, progressEvents);
  assertEquals(1, completeEvents);
}

function testAbortsAndErrors() {
  if (!window.Blob) {
    return;
  }

  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn, 13);
  var blob = new BlobMock('The quick brown fox jumps over the lazy dog');
  var abortEvents = 0;
  var errorEvents = 0;
  var completeEvents = 0;
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.ABORT,
      function() { ++abortEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.ERROR,
      function() { ++errorEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE,
      function() { ++completeEvents; });

  // Immediate abort.
  blobHasher.hash(blob);
  assertEquals(0, abortEvents);
  assertEquals(0, errorEvents);
  assertEquals(0, completeEvents);
  blobHasher.abort();
  blobHasher.abort();
  assertEquals(1, abortEvents);
  assertEquals(0, errorEvents);
  assertEquals(0, completeEvents);
  abortEvents = 0;

  // Delayed abort.
  blobHasher.hash(blob);
  blobHasher.fileReader_.mockLoad();
  assertEquals(0, abortEvents);
  assertEquals(0, errorEvents);
  assertEquals(0, completeEvents);
  blobHasher.abort();
  blobHasher.abort();
  assertEquals(1, abortEvents);
  assertEquals(0, errorEvents);
  assertEquals(0, completeEvents);
  abortEvents = 0;

  // Immediate error.
  blobHasher.hash(blob);
  blobHasher.fileReader_.mockError();
  assertEquals(0, abortEvents);
  assertEquals(1, errorEvents);
  assertEquals(0, completeEvents);
  errorEvents = 0;

  // Delayed error.
  blobHasher.hash(blob);
  blobHasher.fileReader_.mockLoad();
  blobHasher.fileReader_.mockError();
  assertEquals(0, abortEvents);
  assertEquals(1, errorEvents);
  assertEquals(0, completeEvents);
  abortEvents = 0;
}

function testBasicThrottling() {
  if (!window.Blob) {
    return;
  }

  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn, 5);
  var blob = new BlobMock('The quick brown fox jumps over the lazy dog');
  var throttledEvents = 0;
  var completeEvents = 0;
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED,
      function() { ++throttledEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE,
      function() { ++completeEvents; });

  // Start a throttled hash. No chunks should be processed yet.
  blobHasher.setHashingLimit(0);
  assertEquals(0, throttledEvents);
  blobHasher.hash(blob);
  assertEquals(1, throttledEvents);
  assertEquals(0, blobHasher.getBytesProcessed());
  assertNull(blobHasher.fileReader_);

  // One chunk should be processed.
  blobHasher.setHashingLimit(4);
  assertEquals(1, throttledEvents);
  assertEquals(1, readFromBlob(blobHasher, 1));
  assertEquals(2, throttledEvents);
  assertEquals(4, blobHasher.getBytesProcessed());

  // One more chunk should be processed.
  blobHasher.setHashingLimit(5);
  assertEquals(2, throttledEvents);
  assertEquals(1, readFromBlob(blobHasher, 1));
  assertEquals(3, throttledEvents);
  assertEquals(5, blobHasher.getBytesProcessed());

  // Two more chunks should be processed.
  blobHasher.setHashingLimit(15);
  assertEquals(3, throttledEvents);
  assertEquals(2, readFromBlob(blobHasher, 2));
  assertEquals(4, throttledEvents);
  assertEquals(15, blobHasher.getBytesProcessed());

  // The entire blob should be processed.
  blobHasher.setHashingLimit(Infinity);
  var expectedChunks = Math.ceil(blob.size / 5) - 3;
  assertEquals(expectedChunks, readFromBlob(blobHasher, expectedChunks));
  assertEquals(4, throttledEvents);
  assertEquals(1, completeEvents);
  assertEquals(
      '9e107d9d372bb6826bd81d3542a419d6',
      goog.crypt.byteArrayToHex(blobHasher.getHash()));
}

function testLengthZeroThrottling() {
  if (!window.Blob) {
    return;
  }

  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn);
  var throttledEvents = 0;
  var completeEvents = 0;
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED,
      function() { ++throttledEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE,
      function() { ++completeEvents; });

  // Test throttling with length 0 blob.
  var blob = new BlobMock('');
  blobHasher.setHashingLimit(0);
  blobHasher.hash(blob);
  assertEquals(0, throttledEvents);
  assertEquals(1, completeEvents);
  assertEquals(
      'd41d8cd98f00b204e9800998ecf8427e',
      goog.crypt.byteArrayToHex(blobHasher.getHash()));
}

function testAbortsAndErrorsWhileThrottling() {
  if (!window.Blob) {
    return;
  }

  var hashFn = new goog.crypt.Md5();
  var blobHasher = new goog.crypt.BlobHasher(hashFn, 5);
  var blob = new BlobMock('The quick brown fox jumps over the lazy dog');
  var abortEvents = 0;
  var errorEvents = 0;
  var throttledEvents = 0;
  var completeEvents = 0;
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.ABORT,
      function() { ++abortEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.ERROR,
      function() { ++errorEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED,
      function() { ++throttledEvents; });
  goog.events.listen(
      blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE,
      function() { ++completeEvents; });

  // Test that processing cannot be continued after abort.
  blobHasher.setHashingLimit(0);
  blobHasher.hash(blob);
  assertEquals(1, throttledEvents);
  blobHasher.abort();
  assertEquals(1, abortEvents);
  blobHasher.setHashingLimit(10);
  assertNull(blobHasher.fileReader_);
  assertEquals(1, throttledEvents);
  assertEquals(0, completeEvents);
  assertNull(blobHasher.getHash());

  // Test that processing cannot be continued after error.
  blobHasher.hash(blob);
  assertEquals(1, throttledEvents);
  blobHasher.fileReader_.mockError();
  assertEquals(1, errorEvents);
  blobHasher.setHashingLimit(100);
  assertNull(blobHasher.fileReader_);
  assertEquals(1, throttledEvents);
  assertEquals(0, completeEvents);
  assertNull(blobHasher.getHash());
}
