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

// Tests for `History.insertMany` as implemented in History.sys.mjs

"use strict";

add_task(async function test_error_cases() {
  let validPageInfo = {
    url: "http://mozilla.com",
    visits: [{ transition: TRANSITION_LINK }],
  };

  Assert.throws(
    () => PlacesUtils.history.insertMany(),
    /TypeError: pageInfos must be an array/,
    "passing a null into History.insertMany should throw a TypeError"
  );
  Assert.throws(
    () => PlacesUtils.history.insertMany([]),
    /TypeError: pageInfos may not be an empty array/,
    "passing an empty array into History.insertMany should throw a TypeError"
  );
  Assert.throws(
    () => PlacesUtils.history.insertMany([validPageInfo, {}]),
    /Error: PageInfo: The following properties were expected/,
    "passing a second invalid PageInfo object to History.insertMany should throw an Error"
  );
});

add_task(async function test_insertMany() {
  const BAD_URLS = ["about:config", "chrome://browser/content/browser.xhtml"];
  const GOOD_URLS = [1, 2, 3].map(x => {
    return `http://mozilla.com/${x}`;
  });

  let makePageInfos = async function (urls, filter = x => x) {
    let pageInfos = [];
    for (let url of urls) {
      let uri = NetUtil.newURI(url);

      let pageInfo = {
        title: `Visit to ${url}`,
        visits: [{ transition: TRANSITION_LINK }],
      };

      pageInfo.url = await filter(uri);
      pageInfos.push(pageInfo);
    }
    return pageInfos;
  };

  let inserter = async function (name, filter, useCallbacks) {
    info(name);
    info(`filter: ${filter}`);
    info(`useCallbacks: ${useCallbacks}`);
    await PlacesUtils.history.clear();

    let result;
    let allUrls = GOOD_URLS.concat(BAD_URLS);
    let pageInfos = await makePageInfos(allUrls, filter);

    if (useCallbacks) {
      let onResultUrls = [];
      let onErrorUrls = [];
      result = await PlacesUtils.history.insertMany(
        pageInfos,
        pageInfo => {
          let url = pageInfo.url.href;
          Assert.ok(
            GOOD_URLS.includes(url),
            "onResult callback called for correct url"
          );
          onResultUrls.push(url);
          Assert.equal(
            `Visit to ${url}`,
            pageInfo.title,
            "onResult callback provides the correct title"
          );
          Assert.ok(
            PlacesUtils.isValidGuid(pageInfo.guid),
            "onResult callback provides a valid guid"
          );
        },
        pageInfo => {
          let url = pageInfo.url.href;
          Assert.ok(
            BAD_URLS.includes(url),
            "onError callback called for correct uri"
          );
          onErrorUrls.push(url);
          Assert.equal(
            undefined,
            pageInfo.title,
            "onError callback provides the correct title"
          );
          Assert.equal(
            undefined,
            pageInfo.guid,
            "onError callback provides the expected guid"
          );
        }
      );
      Assert.equal(
        GOOD_URLS.sort().toString(),
        onResultUrls.sort().toString(),
        "onResult callback was called for each good url"
      );
      Assert.equal(
        BAD_URLS.sort().toString(),
        onErrorUrls.sort().toString(),
        "onError callback was called for each bad url"
      );
    } else {
      const promiseRankingChanged =
        PlacesTestUtils.waitForNotification("pages-rank-changed");
      result = await PlacesUtils.history.insertMany(pageInfos);
      await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
      await promiseRankingChanged;
    }

    Assert.equal(undefined, result, "insertMany returned undefined");

    for (let url of allUrls) {
      let expected = GOOD_URLS.includes(url);
      Assert.equal(
        expected,
        await PlacesTestUtils.isPageInDB(url),
        `isPageInDB for ${url} is ${expected}`
      );
      Assert.equal(
        expected,
        await PlacesTestUtils.visitsInDB(url),
        `visitsInDB for ${url} is ${expected}`
      );
    }
  };

  try {
    for (let useCallbacks of [false, true]) {
      await inserter(
        "Testing History.insertMany() with an nsIURI",
        x => x,
        useCallbacks
      );
      await inserter(
        "Testing History.insertMany() with a string url",
        x => x.spec,
        useCallbacks
      );
      await inserter(
        "Testing History.insertMany() with a URL object",
        x => URL.fromURI(x),
        useCallbacks
      );
    }
    // Test rejection when no items added
    let pageInfos = await makePageInfos(BAD_URLS);
    PlacesUtils.history.insertMany(pageInfos).then(
      () => {
        Assert.ok(
          false,
          "History.insertMany rejected promise with all bad URLs"
        );
      },
      error => {
        Assert.equal(
          "No items were added to history.",
          error.message,
          "History.insertMany rejected promise with all bad URLs"
        );
      }
    );
  } finally {
    await PlacesUtils.history.clear();
  }
});

add_task(async function test_transitions() {
  const places = Object.keys(PlacesUtils.history.TRANSITIONS).map(
    transition => {
      return {
        url: `http://places.test/${transition}`,
        visits: [{ transition: PlacesUtils.history.TRANSITIONS[transition] }],
      };
    }
  );
  // Should not reject.
  await PlacesUtils.history.insertMany(places);
  // Check callbacks.
  let count = 0;
  await PlacesUtils.history.insertMany(places, () => {
    ++count;
  });
  Assert.equal(count, Object.keys(PlacesUtils.history.TRANSITIONS).length);
});

add_task(async function test_guid() {
  const guidA = "aaaaaaaaaaaa";
  const guidB = "bbbbbbbbbbbb";
  const guidC = "cccccccccccc";

  await PlacesUtils.history.insertMany([
    {
      title: "foo",
      url: "http://example.com/foo",
      guid: guidA,
      visits: [{ transition: TRANSITION_LINK, date: new Date() }],
    },
  ]);

  Assert.ok(
    await PlacesUtils.history.fetch(guidA),
    "Record is inserted with correct GUID"
  );

  let expectedGuids = new Set([guidB, guidC]);
  await PlacesUtils.history.insertMany(
    [
      {
        title: "bar",
        url: "http://example.com/bar",
        guid: guidB,
        visits: [{ transition: TRANSITION_LINK, date: new Date() }],
      },
      {
        title: "baz",
        url: "http://example.com/baz",
        guid: guidC,
        visits: [{ transition: TRANSITION_LINK, date: new Date() }],
      },
    ],
    pageInfo => {
      Assert.ok(expectedGuids.has(pageInfo.guid));
      expectedGuids.delete(pageInfo.guid);
    }
  );
  Assert.equal(expectedGuids.size, 0);

  Assert.ok(
    await PlacesUtils.history.fetch(guidB),
    "Record B is fetchable after insertMany"
  );
  Assert.ok(
    await PlacesUtils.history.fetch(guidC),
    "Record C is fetchable after insertMany"
  );
});

add_task(async function test_withUserPass() {
  await PlacesUtils.history.insertMany([
    {
      url: "http://user:pass@example.com/userpass",
      visits: [{ date: new Date() }],
    },
  ]);

  Assert.ok(
    !(await PlacesUtils.history.fetch("http://user:pass@example.com/userpass")),
    "The url with user and pass is not stored"
  );

  Assert.ok(
    await PlacesUtils.history.fetch("http://example.com/userpass"),
    "The url without user and pass is stored"
  );
});
