/* Any copyright is dedicated to the Public Domain.
 *    http://creativecommons.org/publicdomain/zero/1.0/ */

/*
 * Tests for mozIAsyncFavicons::replaceFaviconData()
 */

let iconsvc = PlacesUtils.favicons;
let histsvc = PlacesUtils.history;

let originalFavicon = {
  file: do_get_file("favicon-normal16.png"),
  uri: uri(do_get_file("favicon-normal16.png")),
  data: readFileData(do_get_file("favicon-normal16.png")),
  mimetype: "image/png"
};

let uniqueFaviconId = 0;
function createFavicon(fileName) {
  let tempdir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);

  // remove any existing file at the path we're about to copy to
  let outfile = tempdir.clone();
  outfile.append(fileName);
  try { outfile.remove(false); } catch (e) {}

  originalFavicon.file.copyToFollowingLinks(tempdir, fileName);

  let stream = Cc["@mozilla.org/network/file-output-stream;1"]
    .createInstance(Ci.nsIFileOutputStream);
  stream.init(outfile, 0x02 | 0x08 | 0x10, 0600, 0);

  // append some data that sniffers/encoders will ignore that will distinguish
  // the different favicons we'll create
  uniqueFaviconId++;
  let uniqueStr = "uid:" + uniqueFaviconId;
  stream.write(uniqueStr, uniqueStr.length);
  stream.close();

  do_check_eq(outfile.leafName.substr(0, fileName.length), fileName);

  return {
    file: outfile,
    uri: uri(outfile),
    data: readFileData(outfile),
    mimetype: "image/png"
  };
}

function createDataURLForFavicon(favicon) {
  return "data:" + favicon.mimetype + ";base64," + toBase64(favicon.data);
}

function checkCallbackSucceeded(callbackMimetype, callbackData, sourceMimetype, sourceData) {
  do_check_eq(callbackMimetype, sourceMimetype);
  do_check_true(compareArrays(callbackData, sourceData));
}

function run_test() {
  // check that the favicon loaded correctly
  do_check_eq(originalFavicon.data.length, 286);
  run_next_test();
};

add_task(function test_replaceFaviconDataFromDataURL_validHistoryURI() {
  do_log_info("test replaceFaviconDataFromDataURL for valid history uri");

  let pageURI = uri("http://test1.bar/");
  yield promiseAddVisits(pageURI);

  let favicon = createFavicon("favicon1.png");
  iconsvc.replaceFaviconDataFromDataURL(favicon.uri, createDataURLForFavicon(favicon));

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
      checkFaviconDataForPage(
        pageURI, favicon.mimetype, favicon.data,
        function test_replaceFaviconDataFromDataURL_validHistoryURI_callback() {
          favicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() {
  do_log_info("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage");

  let pageURI = uri("http://test2.bar/");
  yield promiseAddVisits(pageURI);

  let firstFavicon = createFavicon("favicon2.png");
  let secondFavicon = createFavicon("favicon3.png");

  iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, firstFavicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
      checkFaviconDataForPage(
        pageURI, secondFavicon.mimetype, secondFavicon.data,
        function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_callback() {
          firstFavicon.file.remove(false);
          secondFavicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_replaceExisting() {
  do_log_info("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage");

  let pageURI = uri("http://test3.bar");
  yield promiseAddVisits(pageURI);

  let firstFavicon = createFavicon("favicon4.png");
  let secondFavicon = createFavicon("favicon5.png");

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, firstFavicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_replaceExisting_firstSet_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, firstFavicon.mimetype, firstFavicon.data);
      checkFaviconDataForPage(
        pageURI, firstFavicon.mimetype, firstFavicon.data,
        function test_replaceFaviconDataFromDataURL_replaceExisting_firstCallback() {
          iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));
          checkFaviconDataForPage(
            pageURI, secondFavicon.mimetype, secondFavicon.data,
            function test_replaceFaviconDataFromDataURL_replaceExisting_secondCallback() {
              firstFavicon.file.remove(false);
              secondFavicon.file.remove(false);
              deferSetAndFetchFavicon.resolve();
            });
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_unrelatedReplace() {
  do_log_info("test replaceFaviconDataFromDataURL to not make unrelated changes");

  let pageURI = uri("http://test4.bar/");
  yield promiseAddVisits(pageURI);

  let favicon = createFavicon("favicon6.png");
  let unrelatedFavicon = createFavicon("favicon7.png");

  iconsvc.replaceFaviconDataFromDataURL(unrelatedFavicon.uri, createDataURLForFavicon(unrelatedFavicon));

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, favicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_unrelatedReplace_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
      checkFaviconDataForPage(
        pageURI, favicon.mimetype, favicon.data,
        function test_replaceFaviconDataFromDataURL_unrelatedReplace_callback() {
          favicon.file.remove(false);
          unrelatedFavicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_badInputs() {
  do_log_info("test replaceFaviconDataFromDataURL to throw on bad inputs");

  let favicon = createFavicon("favicon8.png");

  let ex = null;
  try {
    iconsvc.replaceFaviconDataFromDataURL(favicon.uri, "");
  } catch (e) {
    ex = e;
  } finally {
    do_check_true(!!ex);
  }

  ex = null;
  try {
    iconsvc.replaceFaviconDataFromDataURL(null, createDataURLForFavicon(favicon));
  } catch (e) {
    ex = e;
  } finally {
    do_check_true(!!ex);
  }

  favicon.file.remove(false);

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_twiceReplace() {
  do_log_info("test replaceFaviconDataFromDataURL on multiple replacements");

  let pageURI = uri("http://test5.bar/");
  yield promiseAddVisits(pageURI);

  let firstFavicon = createFavicon("favicon9.png");
  let secondFavicon = createFavicon("favicon10.png");

  iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon));
  iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, firstFavicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_twiceReplace_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
      checkFaviconDataForPage(
        pageURI, secondFavicon.mimetype, secondFavicon.data,
        function test_replaceFaviconDataFromDataURL_twiceReplace_callback() {
          firstFavicon.file.remove(false);
          secondFavicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_afterRegularAssign() {
  do_log_info("test replaceFaviconDataFromDataURL after replaceFaviconData");

  let pageURI = uri("http://test6.bar/");
  yield promiseAddVisits(pageURI);

  let firstFavicon = createFavicon("favicon11.png");
  let secondFavicon = createFavicon("favicon12.png");

  iconsvc.replaceFaviconData(
    firstFavicon.uri, firstFavicon.data, firstFavicon.data.length,
    firstFavicon.mimetype);
  iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, firstFavicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_afterRegularAssign_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
      checkFaviconDataForPage(
        pageURI, secondFavicon.mimetype, secondFavicon.data,
        function test_replaceFaviconDataFromDataURL_afterRegularAssign_callback() {
          firstFavicon.file.remove(false);
          secondFavicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

add_task(function test_replaceFaviconDataFromDataURL_beforeRegularAssign() {
  do_log_info("test replaceFaviconDataFromDataURL before replaceFaviconData");

  let pageURI = uri("http://test7.bar/");
  yield promiseAddVisits(pageURI);

  let firstFavicon = createFavicon("favicon13.png");
  let secondFavicon = createFavicon("favicon14.png");

  iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon));
  iconsvc.replaceFaviconData(
    firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
    secondFavicon.mimetype);

  let deferSetAndFetchFavicon = Promise.defer();
  iconsvc.setAndFetchFaviconForPage(
    pageURI, firstFavicon.uri, true,
    PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
    function test_replaceFaviconDataFromDataURL_beforeRegularAssign_check(aURI, aDataLen, aData, aMimeType) {
      checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
      checkFaviconDataForPage(
        pageURI, secondFavicon.mimetype, secondFavicon.data,
        function test_replaceFaviconDataFromDataURL_beforeRegularAssign_callback() {
          firstFavicon.file.remove(false);
          secondFavicon.file.remove(false);
          deferSetAndFetchFavicon.resolve();
        });
    });
  yield deferSetAndFetchFavicon.promise;

  yield promiseClearHistory();
});

/* toBase64 copied from image/test/unit/test_encoder_png.js */

/* Convert data (an array of integers) to a Base64 string. */
const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +
    '0123456789+/';
const base64Pad = '=';
function toBase64(data) {
  let result = '';
  let length = data.length;
  let i;
  // Convert every three bytes to 4 ascii characters.
  for (i = 0; i < (length - 2); i += 3) {
    result += toBase64Table[data[i] >> 2];
    result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)];
    result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)];
    result += toBase64Table[data[i+2] & 0x3f];
  }

  // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
  if (length%3) {
    i = length - (length%3);
    result += toBase64Table[data[i] >> 2];
    if ((length%3) == 2) {
      result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)];
      result += toBase64Table[(data[i+1] & 0x0f) << 2];
      result += base64Pad;
    } else {
      result += toBase64Table[(data[i] & 0x03) << 4];
      result += base64Pad + base64Pad;
    }
  }

  return result;
}
