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

// Makes sure the moz_origins table and origin frecency stats are updated
// correctly.

"use strict";

// Visiting a URL with a new origin should immediately update moz_origins.
add_task(async function visit() {
  await checkDB([]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// Repeatedly visiting a URL with an initially new origin should update
// moz_origins (with the correct frecency).
add_task(async function visitRepeatedly() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// Same as previous, but visits are added sequentially.
add_task(async function visitRepeatedlySequential() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// After removing an origin's URLs, visiting a URL with the origin should
// immediately update moz_origins.
add_task(async function vistAfterDelete() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// Visiting different URLs with the same origin should update moz_origins, and
// moz_origins.frecency should be the sum of the URL frecencies.
add_task(async function visitDifferentURLsSameOrigin() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/1");
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/2");
  await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
  await PlacesUtils.history.remove("http://example.com/3");
  await checkDB([]);
  await cleanUp();
});

// Same as previous, but visits are added sequentially.
add_task(async function visitDifferentURLsSameOriginSequential() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/1"]]]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/1", "http://example.com/2"],
    ],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/1");
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/2");
  await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
  await PlacesUtils.history.remove("http://example.com/3");
  await checkDB([]);
  await cleanUp();
});

// Repeatedly visiting different URLs with the same origin should update
// moz_origins (with the correct frecencies), and moz_origins.frecency should be
// the sum of the URL frecencies.
add_task(async function visitDifferentURLsSameOriginRepeatedly() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/1");
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/2", "http://example.com/3"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/2");
  await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
  await PlacesUtils.history.remove("http://example.com/3");
  await checkDB([]);
  await cleanUp();
});

// Visiting URLs with different origins should update moz_origins.
add_task(async function visitDifferentOrigins() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://example2.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://example3.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/"]],
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/");
  await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
  await PlacesUtils.history.remove("http://example3.com/");
  await checkDB([]);
  await cleanUp();
});

// Same as previous, but visits are added sequentially.
add_task(async function visitDifferentOriginsSequential() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([["http://", "example1.com", ["http://example1.com/"]]]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example2.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/"]],
    ["http://", "example2.com", ["http://example2.com/"]],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example3.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/"]],
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/");
  await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
  await PlacesUtils.history.remove("http://example3.com/");
  await checkDB([]);
  await cleanUp();
});

// Repeatedly visiting URLs with different origins should update moz_origins
// (with the correct frecencies).
add_task(async function visitDifferentOriginsRepeatedly() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example1.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example1.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example2.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example2.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example3.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/"]],
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/"]],
    ["http://", "example3.com", ["http://example3.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/");
  await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
  await PlacesUtils.history.remove("http://example3.com/");
  await checkDB([]);
  await cleanUp();
});

// Visiting URLs, some with the same and some with different origins, should
// update moz_origins.
add_task(async function visitDifferentOriginsDifferentURLs() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example1.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example1.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example2.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example2.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example3.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/1");
  await checkDB([
    [
      "http://",
      "example1.com",
      ["http://example1.com/2", "http://example1.com/3"],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/2");
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/3"]],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/3");
  await checkDB([
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/1");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/2"]],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/2");
  await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
  await PlacesUtils.history.remove("http://example3.com/1");
  await checkDB([]);
});

// Same as previous, but visits are added sequentially.
add_task(async function visitDifferentOriginsDifferentURLsSequential() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([["http://", "example1.com", ["http://example1.com/1"]]]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      ["http://example1.com/1", "http://example1.com/2"],
    ],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example2.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
    ["http://", "example2.com", ["http://example2.com/1"]],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example2.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
  ]);
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example3.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/1");
  await checkDB([
    [
      "http://",
      "example1.com",
      ["http://example1.com/2", "http://example1.com/3"],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/2");
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/3"]],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/3");
  await checkDB([
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/1");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/2"]],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/2");
  await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
  await PlacesUtils.history.remove("http://example3.com/1");
  await checkDB([]);
});

// Repeatedly visiting URLs, some with the same and some with different origins,
// should update moz_origins (with the correct frecencies).
add_task(async function visitDifferentOriginsDifferentURLsRepeatedly() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example1.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example1.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example1.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example1.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example1.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example1.com/3",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example2.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example2.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
    {
      uri: "http://example2.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(2),
    },
    {
      uri: "http://example2.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(3),
    },
    {
      uri: "http://example2.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(4),
    },
    {
      uri: "http://example3.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(0),
    },
    {
      uri: "http://example3.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
      visitDate: daysAgo(1),
    },
  ]);
  await checkDB([
    [
      "http://",
      "example1.com",
      [
        "http://example1.com/1",
        "http://example1.com/2",
        "http://example1.com/3",
      ],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/1");
  await checkDB([
    [
      "http://",
      "example1.com",
      ["http://example1.com/2", "http://example1.com/3"],
    ],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/2");
  await checkDB([
    ["http://", "example1.com", ["http://example1.com/3"]],
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example1.com/3");
  await checkDB([
    [
      "http://",
      "example2.com",
      ["http://example2.com/1", "http://example2.com/2"],
    ],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/1");
  await checkDB([
    ["http://", "example2.com", ["http://example2.com/2"]],
    ["http://", "example3.com", ["http://example3.com/1"]],
  ]);
  await PlacesUtils.history.remove("http://example2.com/2");
  await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
  await PlacesUtils.history.remove("http://example3.com/1");
  await checkDB([]);
});

// Makes sure URIs with the same TLD but different www subdomains are recognized
// as different origins.  Makes sure removing one doesn't remove the others.
add_task(async function www1() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "www.example.com", ["http://www.example.com/"]],
    ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
  ]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([
    ["http://", "www.example.com", ["http://www.example.com/"]],
    ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
  ]);
  await PlacesUtils.history.remove("http://www.example.com/");
  await checkDB([
    ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
  ]);
  await PlacesUtils.history.remove("http://www.www.example.com/");
  await checkDB([]);
  await cleanUp();
});

// Same as www1, but removes URIs in a different order.
add_task(async function www2() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "www.example.com", ["http://www.example.com/"]],
    ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
  ]);
  await PlacesUtils.history.remove("http://www.www.example.com/");
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "www.example.com", ["http://www.example.com/"]],
  ]);
  await PlacesUtils.history.remove("http://www.example.com/");
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// Makes sure removing an origin without a port doesn't remove the same host
// with a port.
add_task(async function ports1() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://example.com:8888/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "example.com:8888", ["http://example.com:8888/"]],
  ]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([
    ["http://", "example.com:8888", ["http://example.com:8888/"]],
  ]);
  await PlacesUtils.history.remove("http://example.com:8888/");
  await checkDB([]);
  await cleanUp();
});

// Makes sure removing an origin with a port doesn't remove the same host
// without a port.
add_task(async function ports2() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://example.com:8888/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "example.com:8888", ["http://example.com:8888/"]],
  ]);
  await PlacesUtils.history.remove("http://example.com:8888/");
  await checkDB([["http://", "example.com", ["http://example.com/"]]]);
  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([]);
  await cleanUp();
});

// Makes sure multiple URIs with the same origin don't create duplicate origins.
add_task(async function duplicates() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://www.www.example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "https://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "ftp://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "foo://example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "bar:example.com/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    {
      uri: "http://example.com:8888/",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    { uri: "http://example.com/dupe" },
    { uri: "http://www.example.com/dupe" },
    { uri: "http://www.www.example.com/dupe" },
    { uri: "https://example.com/dupe" },
    { uri: "ftp://example.com/dupe" },
    { uri: "foo://example.com/dupe" },
    { uri: "bar:example.com/dupe" },
    { uri: "http://example.com:8888/dupe" },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/", "http://example.com/dupe"],
    ],
    [
      "http://",
      "www.example.com",
      ["http://www.example.com/", "http://www.example.com/dupe"],
    ],
    [
      "http://",
      "www.www.example.com",
      ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
    ],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("http://example.com/");
  await checkDB([
    ["http://", "example.com", ["http://example.com/dupe"]],
    [
      "http://",
      "www.example.com",
      ["http://www.example.com/", "http://www.example.com/dupe"],
    ],
    [
      "http://",
      "www.www.example.com",
      ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
    ],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("http://example.com/dupe");
  await checkDB([
    [
      "http://",
      "www.example.com",
      ["http://www.example.com/", "http://www.example.com/dupe"],
    ],
    [
      "http://",
      "www.www.example.com",
      ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
    ],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("http://www.example.com/");
  await checkDB([
    ["http://", "www.example.com", ["http://www.example.com/dupe"]],
    [
      "http://",
      "www.www.example.com",
      ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
    ],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("http://www.example.com/dupe");
  await checkDB([
    [
      "http://",
      "www.www.example.com",
      ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
    ],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("http://www.www.example.com/");
  await checkDB([
    ["http://", "www.www.example.com", ["http://www.www.example.com/dupe"]],
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("http://www.www.example.com/dupe");
  await checkDB([
    [
      "https://",
      "example.com",
      ["https://example.com/", "https://example.com/dupe"],
    ],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("https://example.com/");
  await checkDB([
    ["https://", "example.com", ["https://example.com/dupe"]],
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("https://example.com/dupe");
  await checkDB([
    ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("ftp://example.com/");
  await checkDB([
    ["ftp://", "example.com", ["ftp://example.com/dupe"]],
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("ftp://example.com/dupe");
  await checkDB([
    ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("foo://example.com/");
  await checkDB([
    ["foo://", "example.com", ["foo://example.com/dupe"]],
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("foo://example.com/dupe");
  await checkDB([
    ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("bar:example.com/");
  await checkDB([
    ["bar:", "example.com", ["bar:example.com/dupe"]],
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);
  await PlacesUtils.history.remove("bar:example.com/dupe");
  await checkDB([
    [
      "http://",
      "example.com:8888",
      ["http://example.com:8888/", "http://example.com:8888/dupe"],
    ],
  ]);

  await PlacesUtils.history.remove("http://example.com:8888/");
  await checkDB([
    ["http://", "example.com:8888", ["http://example.com:8888/dupe"]],
  ]);
  await PlacesUtils.history.remove("http://example.com:8888/dupe");
  await checkDB([]);

  await cleanUp();
});

// Makes sure adding and removing bookmarks creates origins.
add_task(async function addRemoveBookmarks() {
  let bookmarks = [];
  let urls = ["http://example.com/", "http://www.example.com/"];
  for (let url of urls) {
    bookmarks.push(
      await PlacesUtils.bookmarks.insert({
        url,
        parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      })
    );
  }
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "www.example.com", ["http://www.example.com/"]],
  ]);
  await PlacesUtils.bookmarks.remove(bookmarks[0]);
  await PlacesUtils.history.clear();
  await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]);
  await PlacesUtils.bookmarks.remove(bookmarks[1]);
  await PlacesUtils.history.clear();
  await checkDB([]);
  await cleanUp();
});

// Makes sure changing bookmarks also changes the corresponding origins.
add_task(async function changeBookmarks() {
  let bookmarks = [];
  let urls = ["http://example.com/", "http://www.example.com/"];
  for (let url of urls) {
    bookmarks.push(
      await PlacesUtils.bookmarks.insert({
        url,
        parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      })
    );
  }
  await checkDB([
    ["http://", "example.com", ["http://example.com/"]],
    ["http://", "www.example.com", ["http://www.example.com/"]],
  ]);
  await PlacesUtils.bookmarks.update({
    url: "http://www.example.com/",
    guid: bookmarks[0].guid,
  });
  await PlacesUtils.history.clear();
  await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]);
  await cleanUp();
});

// A slightly more complex test to make sure origin frecency stats are updated
// when visits and bookmarks are added and removed.
add_task(async function moreOriginFrecencyStats() {
  await checkDB([]);

  // Add a URL 0 visit.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/0",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([["http://", "example.com", ["http://example.com/0"]]]);

  // Add a URL 1 visit.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/1",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1"],
    ],
  ]);

  // Add a URL 2 visit.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1", "http://example.com/2"],
    ],
  ]);

  // Add another URL 2 visit.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1", "http://example.com/2"],
    ],
  ]);

  // Remove URL 2's visits.
  await PlacesUtils.history.remove(["http://example.com/2"]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1"],
    ],
  ]);

  // Bookmark URL 1.
  let bookmark = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    title: "A bookmark",
    url: Services.io.newURI("http://example.com/1"),
  });
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1"],
    ],
  ]);

  // Remove URL 1's visit.
  await PlacesUtils.history.remove(["http://example.com/1"]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/1"],
    ],
  ]);

  // Remove URL 1's bookmark.  Also need to call history.remove() again to
  // remove the URL from moz_places.  Otherwise it sticks around and keeps
  // contributing to the frecency stats.
  await PlacesUtils.bookmarks.remove(bookmark);
  await PlacesUtils.history.remove("http://example.com/1");
  await checkDB([["http://", "example.com", ["http://example.com/0"]]]);

  // Remove URL 0.
  await PlacesUtils.history.remove(["http://example.com/0"]);
  await checkDB([]);

  await cleanUp();
});

add_task(async function test_cutoff() {
  // Add first page with visit.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/0",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
  ]);
  // Add a second page last visited before the cutoff, it should be ignored.
  let visitDate = PlacesUtils.toPRTime(
    new Date(
      new Date().setDate(
        -Services.prefs.getIntPref("places.frecency.originsCutOffDays", 90)
      )
    )
  );
  await PlacesTestUtils.addVisits([{ uri: "http://example.com/1", visitDate }]);
  // Add a third page with visit both before and after the cutoff, should count.
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/2",
      transition: PlacesUtils.history.TRANSITION_TYPED,
    },
    { uri: "http://example.com/2", visitDate },
  ]);
  await checkDB([
    [
      "http://",
      "example.com",
      ["http://example.com/0", "http://example.com/2"],
    ],
  ]);
  await cleanUp();
});

/**
 * Returns the expected frecency of the origin of the given URLs.
 * Each URL is expected to have the same origin.
 *
 * @param {(string|nsIURL|URL)[]} urls
 *   An array of URL strings.
 * @returns {number}
 *   The expected origin frecency.
 */
async function expectedOriginFrecency(urls) {
  if (!urls.length) {
    return 1.0;
  }

  // Calculate cutoff: start of today minus N days, in microseconds.
  let cutOffDays = Services.prefs.getIntPref(
    "places.frecency.originsFrecencyCutOffDays",
    90
  );
  let cutOff = new Date();
  cutOff.setHours(0, 0, 0, 0);
  cutOff.setDate(cutOff.getDate() - cutOffDays);
  let cutOffMicroseconds = cutOff.getTime() * 1000;

  let db = await PlacesUtils.promiseDBConnection();

  let distinctDays = new Set();

  let weightedSum = 0;
  let totalTypedVisits = 0;

  for (let url of urls) {
    let frecency = await PlacesTestUtils.getDatabaseValue(
      "moz_places",
      "frecency",
      { url, last_visit_date: [">", cutOffMicroseconds] }
    );
    if (frecency === undefined) {
      continue;
    }

    let rows = await db.executeCached(
      `
      SELECT v.visit_date
      FROM moz_places h
      JOIN moz_historyvisits v ON v.place_id = h.id
      WHERE h.url = :url
        AND v.visit_type = :typed
        AND v.visit_date > :cutoff
    `,
      {
        url,
        typed: PlacesUtils.history.TRANSITION_TYPED,
        cutoff: cutOffMicroseconds,
      }
    );

    if (!rows) {
      continue;
    }

    totalTypedVisits += rows.length;
    weightedSum += frecency * rows.length;

    for (let row of rows) {
      let visitDate = row.getResultByName("visit_date"); // microseconds
      // SQL: date(visit_date/1e6, 'unixepoch') => UTC day
      let day = new Date(visitDate / 1000).toISOString().slice(0, 10);
      distinctDays.add(day);
    }
  }

  if (!totalTypedVisits || !distinctDays.size) {
    return 1.0;
  }

  let average = weightedSum / totalTypedVisits;
  let total = Math.trunc(average * distinctDays.size);

  return total || 1.0;
}

/**
 * Asserts that the moz_origins table and the origin frecency stats are correct.
 *
 * @param {[string, string, (string|nsIURI|URL)[]]} expectedOrigins
 *   An array of expected origins.  Each origin in the array is itself an
 *   array that looks like this:
 *     [prefix, host, [url1, url2, ..., urln]]
 *   The element at index 2 is an array of all the URLs with the origin.
 *   If you don't care about checking frecencies and origin frecency stats,
 *   this element can be `undefined`.
 */
async function checkDB(expectedOrigins) {
  await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();

  let db = await PlacesUtils.promiseDBConnection();
  let rows = await db.execute(`
    SELECT prefix, host, frecency
    FROM moz_origins
    ORDER BY id ASC
  `);
  let checkFrecencies =
    !expectedOrigins.length || expectedOrigins[0][2] !== undefined;
  let actualOrigins = rows.map(row => {
    let o = [];
    for (let c = 0; c < (checkFrecencies ? 3 : 2); c++) {
      o.push(row.getResultByIndex(c));
    }
    return o;
  });
  let expected = [];
  for (let origin of expectedOrigins) {
    expected.push(
      origin
        .slice(0, 2)
        .concat(checkFrecencies ? await expectedOriginFrecency(origin[2]) : [])
    );
  }
  Assert.deepEqual(actualOrigins, expected);
  if (checkFrecencies) {
    info("Checking threshold");
    await PlacesTestUtils.dumpTable({ db, table: "moz_origins" });
    await checkThreshold(expected.map(o => o[2]));
  }
}

/**
 * Asserts that the origin frecency threshold is correct.
 *
 * @param {number[]} expectedOriginFrecencies
 *   An array of expected origin frecencies.
 */
async function checkThreshold(expectedOriginFrecencies) {
  const DEFAULT_THRESHOLD = 2.0;
  let threshold = await PlacesUtils.metadata.get(
    "origin_frecency_threshold",
    DEFAULT_THRESHOLD
  );

  // The origin frecency threshold is calculated by first filtering results
  // above 1. Then we look for the midpoint value of the sorted set.
  // If none is found, we default to the default threshold.
  //
  // This approximates median() which we'll likely use in the future, but
  // differs in two ways:
  // - We filter values starting with 1 because it likely means we deliberately
  //   gave it a very low frecency.
  // - With even-length arrays we use the lower of the two middle values
  //   instead of their average.
  let filteredResults = expectedOriginFrecencies.filter(value => value > 1);
  filteredResults.sort((a, b) => a - b);
  let middle = Math.ceil(filteredResults.length / 2);
  let maxOfLowerGroup = filteredResults.at(middle - 1);

  Assert.equal(
    threshold,
    maxOfLowerGroup ?? DEFAULT_THRESHOLD,
    "Threshold is equal."
  );
}

async function cleanUp() {
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesUtils.history.clear();
}
