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

const { Weave } = ChromeUtils.importESModule(
  "resource://services-sync/main.sys.mjs"
);
const { SyncedTabs } = ChromeUtils.importESModule(
  "resource://services-sync/SyncedTabs.sys.mjs"
);

Log.repository.getLogger("Sync.RemoteTabs").addAppender(new Log.DumpAppender());

// A mock "Tabs" engine which the SyncedTabs module will use instead of the real
// engine. We pass a constructor that Sync creates.
function MockTabsEngine() {
  this.clients = {}; // We'll set this dynamically
  // Mock fxAccounts + recentDeviceList as if we hit the FxA server
  this.fxAccounts = {
    device: {
      recentDeviceList: [
        {
          id: 1,
          name: "updated desktop name",
          availableCommands: {
            "https://identity.mozilla.com/cmd/open-uri": "baz",
          },
        },
        {
          id: 2,
          name: "updated mobile name",
          availableCommands: {
            "https://identity.mozilla.com/cmd/open-uri": "boo",
          },
        },
      ],
    },
  };
}

MockTabsEngine.prototype = {
  name: "tabs",
  enabled: true,

  getAllClients() {
    return Object.values(this.clients);
  },

  getOpenURLs() {
    return new Set();
  },
};

let tabsEngine;

// A clients engine that doesn't need to be a constructor.
let MockClientsEngine = {
  clientSettings: null, // Set in `configureClients`.

  isMobile(guid) {
    if (!guid.endsWith("desktop") && !guid.endsWith("mobile")) {
      throw new Error(
        "this module expected guids to end with 'desktop' or 'mobile'"
      );
    }
    return guid.endsWith("mobile");
  },
  remoteClientExists(id) {
    return this.clientSettings[id] !== false;
  },
  getClientName(id) {
    if (this.clientSettings[id]) {
      return this.clientSettings[id];
    }
    let client = tabsEngine.clients[id];
    let fxaDevice = tabsEngine.fxAccounts.device.recentDeviceList.find(
      device => device.id === client.fxaDeviceId
    );
    return fxaDevice ? fxaDevice.name : client.clientName;
  },

  getClientFxaDeviceId(id) {
    if (this.clientSettings[id]) {
      return this.clientSettings[id];
    }
    return tabsEngine.clients[id].fxaDeviceId;
  },

  getClientType(id) {
    return "desktop";
  },
};

function configureClients(clients, clientSettings = {}) {
  // each client record is expected to have an id.
  for (let [guid, client] of Object.entries(clients)) {
    client.id = guid;
  }
  tabsEngine.clients = clients;
  // Apply clients collection overrides.
  MockClientsEngine.clientSettings = clientSettings;
  // Send an observer that pretends the engine just finished a sync.
  Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
}

add_task(async function setup() {
  await Weave.Service.promiseInitialized;
  // Configure Sync with our mock tabs engine and force it to become initialized.
  await Weave.Service.engineManager.unregister("tabs");
  await Weave.Service.engineManager.register(MockTabsEngine);
  Weave.Service.clientsEngine = MockClientsEngine;
  tabsEngine = Weave.Service.engineManager.get("tabs");

  // Tell the Sync XPCOM service it is initialized.
  let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
    Ci.nsISupports
  ).wrappedJSObject;
  weaveXPCService.ready = true;
});

// The tests.
add_task(async function test_noClients() {
  // no clients, can't be tabs.
  await configureClients({});

  let tabs = await SyncedTabs.getTabClients();
  equal(Object.keys(tabs).length, 0);
});

add_task(async function test_clientWithTabs() {
  await configureClients({
    guid_desktop: {
      clientName: "My Desktop",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          icon: "http://foo.com/favicon",
          lastUsed: 1655745700, // Mon, 20 Jun 2022 17:21:40 GMT
        },
      ],
    },
    guid_mobile: {
      clientName: "My Phone",
      tabs: [],
    },
  });

  let clients = await SyncedTabs.getTabClients();
  equal(clients.length, 2);
  clients.sort((a, b) => {
    return a.name.localeCompare(b.name);
  });
  equal(clients[0].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
  equal(clients[0].tabs[0].icon, "http://foo.com/favicon");
  equal(clients[0].tabs[0].lastUsed, 1655745700);
  // second client has no tabs.
  equal(clients[1].tabs.length, 0);
});

add_task(async function test_staleClientWithTabs() {
  await configureClients(
    {
      guid_desktop: {
        clientName: "My Desktop",
        tabs: [
          {
            urlHistory: ["http://foo.com/"],
            icon: "http://foo.com/favicon",
            lastUsed: 1655745750,
          },
        ],
      },
      guid_mobile: {
        clientName: "My Phone",
        tabs: [],
      },
      guid_stale_mobile: {
        clientName: "My Deleted Phone",
        tabs: [],
      },
      guid_stale_desktop: {
        clientName: "My Deleted Laptop",
        tabs: [
          {
            urlHistory: ["https://bar.com/"],
            icon: "https://bar.com/favicon",
            lastUsed: 1655745700,
          },
        ],
      },
      guid_stale_name_desktop: {
        clientName: "My Generic Device",
        tabs: [
          {
            urlHistory: ["https://example.edu/"],
            icon: "https://example.edu/favicon",
            lastUsed: 1655745800,
          },
        ],
      },
    },
    {
      guid_stale_mobile: false,
      guid_stale_desktop: false,
      // We should always use the device name from the clients collection, instead
      // of the possibly stale tabs collection.
      guid_stale_name_desktop: "My Laptop",
    }
  );
  let clients = await SyncedTabs.getTabClients();
  clients.sort((a, b) => {
    return a.name.localeCompare(b.name);
  });
  equal(clients.length, 3);
  equal(clients[0].name, "My Desktop");
  equal(clients[0].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
  equal(clients[0].tabs[0].lastUsed, 1655745750);
  equal(clients[1].name, "My Laptop");
  equal(clients[1].tabs.length, 1);
  equal(clients[1].tabs[0].url, "https://example.edu/");
  equal(clients[1].tabs[0].lastUsed, 1655745800);
  equal(clients[2].name, "My Phone");
  equal(clients[2].tabs.length, 0);
});

add_task(async function test_clientWithTabsIconsDisabled() {
  Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false);
  await configureClients({
    guid_desktop: {
      clientName: "My Desktop",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          icon: "http://foo.com/favicon",
        },
      ],
    },
  });

  let clients = await SyncedTabs.getTabClients();
  equal(clients.length, 1);
  clients.sort((a, b) => {
    return a.name.localeCompare(b.name);
  });
  equal(clients[0].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
  // Expect the default favicon due to the pref being false.
  equal(clients[0].tabs[0].icon, "page-icon:http://foo.com/");
  Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons");
});

add_task(async function test_filter() {
  // Nothing matches.
  await configureClients({
    guid_desktop: {
      clientName: "My Desktop",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          title: "A test page.",
        },
        {
          urlHistory: ["http://bar.com/"],
          title: "Another page.",
        },
      ],
    },
  });

  let clients = await SyncedTabs.getTabClients("foo");
  equal(clients.length, 1);
  equal(clients[0].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
  // check it matches the title.
  clients = await SyncedTabs.getTabClients("test");
  equal(clients.length, 1);
  equal(clients[0].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
});

add_task(async function test_duplicatesTabsAcrossClients() {
  await configureClients({
    guid_desktop: {
      clientName: "My Desktop",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          title: "A test page.",
        },
      ],
    },
    guid_mobile: {
      clientName: "My Phone",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          title: "A test page.",
        },
      ],
    },
  });

  let clients = await SyncedTabs.getTabClients();
  equal(clients.length, 2);
  equal(clients[0].tabs.length, 1);
  equal(clients[1].tabs.length, 1);
  equal(clients[0].tabs[0].url, "http://foo.com/");
  equal(clients[1].tabs[0].url, "http://foo.com/");
});

add_task(async function test_clientsTabUpdatedName() {
  // See the "fxAccounts" object in the MockEngine above for the device list
  await configureClients({
    guid_desktop: {
      clientName: "My Desktop",
      tabs: [
        {
          urlHistory: ["http://foo.com/"],
          icon: "http://foo.com/favicon",
        },
      ],
      fxaDeviceId: 1,
    },
    guid_mobile: {
      clientName: "My Phone",
      tabs: [
        {
          urlHistory: ["http://bar.com/"],
          icon: "http://bar.com/favicon",
        },
      ],
      fxaDeviceId: 2,
    },
  });
  let clients = await SyncedTabs.getTabClients();
  equal(clients.length, 2);
  equal(clients[0].name, "updated desktop name");
  equal(clients[1].name, "updated mobile name");
});
