/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */

// Tests for `History.insert` and `History.insertMany`, as implemented in History.jsm

"use strict";

add_task(function* test_insert_error_cases() {
  const TEST_URL = "http://mozilla.com";

  let validPageInfo = {
    url: TEST_URL,
    visits: [
      {transition: TRANSITION_LINK}
    ]
  };

  Assert.throws(
    () =>  PlacesUtils.history.insert(),
    /TypeError: pageInfo must be an object/,
    "passing a null into History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert(1),
    /TypeError: pageInfo must be an object/,
    "passing a non object into History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({}),
    /TypeError: PageInfo object must have a url property/,
    "passing an object without a url to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({url: 123}),
    /TypeError: Invalid url or guid: 123/,
    "passing an object with an invalid url to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({url: TEST_URL}),
    /TypeError: PageInfo object must have an array of visits/,
    "passing an object without a visits property to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({url: TEST_URL, visits: 1}),
    /TypeError: PageInfo object must have an array of visits/,
    "passing an object with a non-array visits property to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({url: TEST_URL, visits: []}),
    /TypeError: PageInfo object must have an array of visits/,
    "passing an object with an empty array as the visits property to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({
      url: TEST_URL,
      visits: [
        {
          transition: TRANSITION_LINK,
          date: "a"
        }
      ]}),
    /TypeError: Expected a Date, got a/,
    "passing a visit object with an invalid date to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({
      url: TEST_URL,
      visits: [
        {
          transition: TRANSITION_LINK
        },
        {
          transition: TRANSITION_LINK,
          date: "a"
        }
      ]}),
    /TypeError: Expected a Date, got a/,
    "passing a second visit object with an invalid date to History.insert should throw a TypeError"
  );
  let futureDate = new Date();
  futureDate.setDate(futureDate.getDate() + 1);
  Assert.throws(
    () =>  PlacesUtils.history.insert({
      url: TEST_URL,
      visits: [
        {
          transition: TRANSITION_LINK,
          date: futureDate,
        }
      ]}),
    `TypeError: date: ${futureDate} is not a valid date`,
    "passing a visit object with a future date to History.insert should throw a TypeError"
  );
  Assert.throws(
    () =>  PlacesUtils.history.insert({
      url: TEST_URL,
      visits: [
        {transition: "a"}
      ]}),
    /TypeError: transition: a is not a valid transition type/,
    "passing a visit object with an invalid transition to History.insert should throw a TypeError"
  );
});

add_task(function* test_history_insert() {
  const TEST_URL = "http://mozilla.com/";

  let inserter = Task.async(function*(name, filter, referrer, date, transition) {
    do_print(name);
    do_print(`filter: ${filter}, referrer: ${referrer}, date: ${date}, transition: ${transition}`);

    let uri = NetUtil.newURI(TEST_URL + Math.random());
    let title = "Visit " + Math.random();

    let pageInfo = {
      title,
      visits: [
        {transition: transition, referrer: referrer, date: date, }
      ]
    };

    pageInfo.url = yield filter(uri);

    let result = yield PlacesUtils.history.insert(pageInfo);

    Assert.ok(PlacesUtils.isValidGuid(result.guid), "guid for pageInfo object is valid");
    Assert.equal(uri.spec, result.url.href, "url is correct for pageInfo object");
    Assert.equal(title, result.title, "title is correct for pageInfo object");
    Assert.equal(TRANSITION_LINK, result.visits[0].transition, "transition is correct for pageInfo object");
    if (referrer) {
      Assert.equal(referrer, result.visits[0].referrer.href, "url of referrer for visit is correct");
    } else {
      Assert.equal(null, result.visits[0].referrer, "url of referrer for visit is correct");
    }
    if (date) {
      Assert.equal(Number(date),
                   Number(result.visits[0].date),
                   "date of visit is correct");
    }

    Assert.ok(yield PlacesTestUtils.isPageInDB(uri), "Page was added");
    Assert.ok(yield PlacesTestUtils.visitsInDB(uri), "Visit was added");
  });

  try {
    for (let referrer of [TEST_URL, null]) {
      for (let date of [new Date(), null]) {
        for (let transition of [TRANSITION_LINK, null]) {
          yield inserter("Testing History.insert() with an nsIURI", x => x, referrer, date, transition);
          yield inserter("Testing History.insert() with a string url", x => x.spec, referrer, date, transition);
          yield inserter("Testing History.insert() with a URL object", x => new URL(x.spec), referrer, date, transition);
        }
      }
    }
  } finally {
    yield PlacesTestUtils.clearHistory();
  }
});

add_task(function* test_insert_multiple_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, {}]),
    /TypeError: PageInfo object must have a url property/,
    "passing a second invalid PageInfo object to History.insertMany should throw a TypeError"
  );
});

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

  let makePageInfos = Task.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 = yield filter(uri);
      pageInfos.push(pageInfo);
    }
    return pageInfos;
  });

  let inserter = Task.async(function*(name, filter, useCallbacks) {
    do_print(name);
    do_print(`filter: ${filter}`);
    do_print(`useCallbacks: ${useCallbacks}`);
    yield PlacesTestUtils.clearHistory();

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

    if (useCallbacks) {
      let onResultUrls = [];
      let onErrorUrls = [];
      result = yield 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 {
      result = yield PlacesUtils.history.insertMany(pageInfos);
    }

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

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

  try {
    for (let useCallbacks of [false, true]) {
      yield inserter("Testing History.insertMany() with an nsIURI", x => x, useCallbacks);
      yield inserter("Testing History.insertMany() with a string url", x => x.spec, useCallbacks);
      yield inserter("Testing History.insertMany() with a URL object", x => new URL(x.spec), useCallbacks);
    }
    // Test rejection when no items added
    let pageInfos = yield 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 {
    yield PlacesTestUtils.clearHistory();
  }
});
