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

/*
 * Tests for replaceFaviconData()
 */

var iconsvc = PlacesUtils.favicons;

var 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",
};

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

  // 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, 0o600, 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();

  Assert.equal(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
) {
  Assert.equal(callbackMimetype, sourceMimetype);
  Assert.ok(compareArrays(callbackData, sourceData));
}

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

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

  let pageURI = uri("http://test1.bar/");
  await PlacesTestUtils.addVisits(pageURI);

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

  await new Promise(resolve => {
    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);
            resolve();
          }
        );
      },
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  });

  await PlacesUtils.history.clear();
});

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

    let pageURI = uri("http://test2.bar/");
    await PlacesTestUtils.addVisits(pageURI);

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

    iconsvc.replaceFaviconDataFromDataURL(
      firstFavicon.uri,
      createDataURLForFavicon(secondFavicon),
      0,
      Services.scriptSecurityManager.getSystemPrincipal()
    );

    await new Promise(resolve => {
      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);
              resolve();
            }
          );
        },
        Services.scriptSecurityManager.getSystemPrincipal()
      );
    });

    await PlacesUtils.history.clear();
  }
);

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

  let pageURI = uri("http://test3.bar");
  await PlacesTestUtils.addVisits(pageURI);

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

  await new Promise(resolve => {
    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),
              0,
              Services.scriptSecurityManager.getSystemPrincipal()
            );
            checkFaviconDataForPage(
              pageURI,
              secondFavicon.mimetype,
              secondFavicon.data,
              function test_replaceFaviconDataFromDataURL_replaceExisting_secondCallback() {
                firstFavicon.file.remove(false);
                secondFavicon.file.remove(false);
                resolve();
              }
            );
          }
        );
      },
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  });

  await PlacesUtils.history.clear();
});

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

  let pageURI = uri("http://test4.bar/");
  await PlacesTestUtils.addVisits(pageURI);

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

  iconsvc.replaceFaviconDataFromDataURL(
    unrelatedFavicon.uri,
    createDataURLForFavicon(unrelatedFavicon),
    0,
    Services.scriptSecurityManager.getSystemPrincipal()
  );

  await new Promise(resolve => {
    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);
            resolve();
          }
        );
      },
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  });

  await PlacesUtils.history.clear();
});

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

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

  let ex = null;
  try {
    iconsvc.replaceFaviconDataFromDataURL(
      favicon.uri,
      "",
      0,
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  } catch (e) {
    ex = e;
  } finally {
    Assert.ok(!!ex);
  }

  ex = null;
  try {
    iconsvc.replaceFaviconDataFromDataURL(
      null,
      createDataURLForFavicon(favicon),
      0,
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  } catch (e) {
    ex = e;
  } finally {
    Assert.ok(!!ex);
  }

  favicon.file.remove(false);

  await PlacesUtils.history.clear();
});

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

  let pageURI = uri("http://test5.bar/");
  await PlacesTestUtils.addVisits(pageURI);

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

  iconsvc.replaceFaviconDataFromDataURL(
    firstFavicon.uri,
    createDataURLForFavicon(firstFavicon),
    0,
    Services.scriptSecurityManager.getSystemPrincipal()
  );
  iconsvc.replaceFaviconDataFromDataURL(
    firstFavicon.uri,
    createDataURLForFavicon(secondFavicon),
    0,
    Services.scriptSecurityManager.getSystemPrincipal()
  );

  await new Promise(resolve => {
    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);
            resolve();
          }
        );
      },
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  });

  await PlacesUtils.history.clear();
});

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

    let pageURI = uri("http://test6.bar/");
    await PlacesTestUtils.addVisits(pageURI);

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

    iconsvc.replaceFaviconData(
      firstFavicon.uri,
      firstFavicon.data,
      firstFavicon.mimetype
    );
    iconsvc.replaceFaviconDataFromDataURL(
      firstFavicon.uri,
      createDataURLForFavicon(secondFavicon),
      0,
      Services.scriptSecurityManager.getSystemPrincipal()
    );

    await new Promise(resolve => {
      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);
              resolve();
            }
          );
        },
        Services.scriptSecurityManager.getSystemPrincipal()
      );
    });

    await PlacesUtils.history.clear();
  }
);

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

    let pageURI = uri("http://test7.bar/");
    await PlacesTestUtils.addVisits(pageURI);

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

    iconsvc.replaceFaviconDataFromDataURL(
      firstFavicon.uri,
      createDataURLForFavicon(firstFavicon),
      0,
      Services.scriptSecurityManager.getSystemPrincipal()
    );
    iconsvc.replaceFaviconData(
      firstFavicon.uri,
      secondFavicon.data,
      secondFavicon.mimetype
    );

    await new Promise(resolve => {
      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);
              resolve();
            }
          );
        },
        Services.scriptSecurityManager.getSystemPrincipal()
      );
    });

    await PlacesUtils.history.clear();
  }
);

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

/* Convert data (an array of integers) to a Base64 string. */
const toBase64Table =
  // eslint-disable-next-line no-useless-concat
  "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;
}
