const { ASRouterTriggerListeners } = ChromeUtils.import(
  "resource://activity-stream/lib/ASRouterTriggerListeners.jsm"
);

const mockIdleService = {
  _observers: new Set(),
  _fireObservers(state) {
    for (let observer of this._observers.values()) {
      observer.observe(this, state, null);
    }
  },
  QueryInterface: ChromeUtils.generateQI(["nsIUserIdleService"]),
  idleTime: 1200000,
  addIdleObserver(observer, time) {
    this._observers.add(observer);
  },
  removeIdleObserver(observer, time) {
    this._observers.delete(observer);
  },
};

const sleepMs = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms)); // eslint-disable-line mozilla/no-arbitrary-setTimeout

const inChaosMode = !!parseInt(Services.env.get("MOZ_CHAOSMODE"), 16);

add_setup(async function () {
  // Runtime increases in chaos mode on Mac.
  if (inChaosMode && AppConstants.platform === "macosx") {
    requestLongerTimeout(2);
  }

  registerCleanupFunction(() => {
    const trigger = ASRouterTriggerListeners.get("openURL");
    trigger.uninit();
  });
});

add_task(async function test_openURL_visit_counter() {
  const trigger = ASRouterTriggerListeners.get("openURL");
  const stub = sinon.stub();
  trigger.uninit();

  trigger.init(stub, ["example.com"]);

  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("https://example.com/");
  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("http://example.com/");
  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("http://example.com/");

  Assert.equal(stub.callCount, 3, "Stub called 3 times for example.com host");
  Assert.equal(
    stub.firstCall.args[1].context.visitsCount,
    1,
    "First call should have count 1"
  );
  Assert.equal(
    stub.thirdCall.args[1].context.visitsCount,
    2,
    "Third call should have count 2 for http://example.com"
  );
});

add_task(async function test_openURL_visit_counter_withPattern() {
  const trigger = ASRouterTriggerListeners.get("openURL");
  const stub = sinon.stub();
  trigger.uninit();

  // Match any valid URL
  trigger.init(stub, [], ["*://*/*"]);

  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("https://example.com/");
  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("http://example.com/");
  await waitForUrlLoad("about:blank");
  await waitForUrlLoad("http://example.com/");

  Assert.equal(stub.callCount, 3, "Stub called 3 times for example.com host");
  Assert.equal(
    stub.firstCall.args[1].context.visitsCount,
    1,
    "First call should have count 1"
  );
  Assert.equal(
    stub.thirdCall.args[1].context.visitsCount,
    2,
    "Third call should have count 2 for http://example.com"
  );
});

add_task(async function test_captivePortalLogin() {
  const stub = sinon.stub();
  const captivePortalTrigger =
    ASRouterTriggerListeners.get("captivePortalLogin");

  captivePortalTrigger.init(stub);

  Services.obs.notifyObservers(this, "captive-portal-login-success", {});

  Assert.ok(stub.called, "Called after login event");

  captivePortalTrigger.uninit();

  Services.obs.notifyObservers(this, "captive-portal-login-success", {});

  Assert.equal(stub.callCount, 1, "Not called after uninit");
});

add_task(async function test_preferenceObserver() {
  const stub = sinon.stub();
  const poTrigger = ASRouterTriggerListeners.get("preferenceObserver");

  poTrigger.uninit();

  poTrigger.init(stub, ["foo.bar", "bar.foo"]);

  Services.prefs.setStringPref("foo.bar", "foo.bar");

  Assert.ok(stub.calledOnce, "Called for pref foo.bar");
  Assert.deepEqual(
    stub.firstCall.args[1],
    {
      id: "preferenceObserver",
      param: { type: "foo.bar" },
    },
    "Called with expected arguments"
  );

  Services.prefs.setStringPref("bar.foo", "bar.foo");
  Assert.ok(stub.calledTwice, "Called again for second pref.");
  Services.prefs.clearUserPref("foo.bar");
  Assert.ok(stub.calledThrice, "Called when clearing the pref as well.");

  stub.resetHistory();
  poTrigger.uninit();

  Services.prefs.clearUserPref("bar.foo");
  Assert.ok(stub.notCalled, "Not called after uninit");
});

add_task(async function test_nthTabClosed() {
  const handlerStub = sinon.stub();
  const tabClosedTrigger = ASRouterTriggerListeners.get("nthTabClosed");
  tabClosedTrigger.uninit();
  tabClosedTrigger.init(handlerStub);

  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);

  BrowserTestUtils.removeTab(tab1);
  Assert.ok(handlerStub.calledOnce, "Called once after first tab closed");

  BrowserTestUtils.removeTab(tab2);
  Assert.ok(handlerStub.calledTwice, "Called twice after second tab closed");

  handlerStub.resetHistory();
  tabClosedTrigger.uninit();

  Assert.ok(handlerStub.notCalled, "Not called after uninit");
});

add_task(async function test_cookieBannerDetected() {
  const handlerStub = sinon.stub();
  const bannerDetectedTrigger = ASRouterTriggerListeners.get(
    "cookieBannerDetected"
  );
  bannerDetectedTrigger.uninit();
  bannerDetectedTrigger.init(handlerStub);

  const win = await BrowserTestUtils.openNewBrowserWindow();
  let eventWait = BrowserTestUtils.waitForEvent(win, "cookiebannerdetected");
  win.dispatchEvent(new Event("cookiebannerdetected"));
  await eventWait;
  let closeWindow = BrowserTestUtils.closeWindow(win);

  Assert.ok(
    handlerStub.called,
    "Called after `cookiebannerdetected` event fires"
  );

  handlerStub.resetHistory();
  bannerDetectedTrigger.uninit();

  Assert.ok(handlerStub.notCalled, "Not called after uninit");
  await closeWindow;
});

function getIdleTriggerMock() {
  const idleTrigger = ASRouterTriggerListeners.get("activityAfterIdle");
  idleTrigger.uninit();
  const sandbox = sinon.createSandbox();
  const handlerStub = sandbox.stub();
  sandbox.stub(idleTrigger, "_triggerDelay").value(0);
  sandbox.stub(idleTrigger, "_wakeDelay").value(30);
  sandbox.stub(idleTrigger, "_idleService").value(mockIdleService);
  let restored = false;
  const restore = () => {
    if (restored) return;
    restored = true;
    idleTrigger.uninit();
    sandbox.restore();
  };
  registerCleanupFunction(restore);
  idleTrigger.init(handlerStub);
  return { idleTrigger, handlerStub, restore };
}

// Test that the trigger fires under normal conditions.
add_task(async function test_activityAfterIdle() {
  const { handlerStub, restore } = getIdleTriggerMock();
  let firedOnActive = new Promise(resolve =>
    handlerStub.callsFake(() => resolve(true))
  );
  mockIdleService._fireObservers("idle");
  await TestUtils.waitForTick();
  ok(handlerStub.notCalled, "Not called when idle");
  mockIdleService._fireObservers("active");
  ok(await firedOnActive, "Called once when active after idle");
  restore();
});

// Test that the trigger does not fire when the active window is private.
add_task(async function test_activityAfterIdlePrivateWindow() {
  const { handlerStub, restore } = getIdleTriggerMock();
  let privateWin = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  ok(PrivateBrowsingUtils.isWindowPrivate(privateWin), "Window is private");
  await TestUtils.waitForTick();
  mockIdleService._fireObservers("idle");
  await TestUtils.waitForTick();
  mockIdleService._fireObservers("active");
  await TestUtils.waitForTick();
  ok(handlerStub.notCalled, "Not called when active window is private");
  await BrowserTestUtils.closeWindow(privateWin);
  restore();
});

// Test that the trigger does not fire when the window is minimized, but does
// fire after the window is restored.
add_task(async function test_activityAfterIdleHiddenWindow() {
  const { handlerStub, restore } = getIdleTriggerMock();
  let firedOnRestore = new Promise(resolve =>
    handlerStub.callsFake(() => resolve(true))
  );
  window.minimize();
  await BrowserTestUtils.waitForCondition(
    () => window.windowState === window.STATE_MINIMIZED,
    "Window should be minimized"
  );
  mockIdleService._fireObservers("idle");
  await TestUtils.waitForTick();
  mockIdleService._fireObservers("active");
  await TestUtils.waitForTick();
  ok(handlerStub.notCalled, "Not called when window is minimized");
  window.restore();
  ok(await firedOnRestore, "Called once after restoring minimized window");
  restore();
});

// Test that the trigger does not fire immediately after waking from sleep.
add_task(async function test_activityAfterIdleWake() {
  const { handlerStub, restore } = getIdleTriggerMock();
  let firedAfterWake = new Promise(resolve =>
    handlerStub.callsFake(() => resolve(true))
  );
  mockIdleService._fireObservers("wake_notification");
  mockIdleService._fireObservers("idle");
  await sleepMs(1);
  mockIdleService._fireObservers("active");
  await sleepMs(inChaosMode ? 32 : 300);
  ok(handlerStub.notCalled, "Not called immediately after waking from sleep");

  mockIdleService._fireObservers("idle");
  await TestUtils.waitForTick();
  mockIdleService._fireObservers("active");
  ok(
    await firedAfterWake,
    "Called once after waiting for wake delay before firing idle"
  );
  restore();
});

add_task(async function test_formAutofillTrigger() {
  const sandbox = sinon.createSandbox();
  const handlerStub = sandbox.stub();
  const formAutofillTrigger = ASRouterTriggerListeners.get("formAutofill");
  sandbox.stub(formAutofillTrigger, "_triggerDelay").value(0);
  formAutofillTrigger.uninit();
  formAutofillTrigger.init(handlerStub);

  function notifyCreditCardSaved() {
    Services.obs.notifyObservers(
      {
        wrappedJSObject: { sourceSync: false, collectionName: "creditCards" },
      },
      formAutofillTrigger._topic,
      "add"
    );
  }

  // Saving credit cards for autofill currently fails for some hardware
  // configurations, so mock the event instead of really adding a card.
  notifyCreditCardSaved();
  await sleepMs(1);
  Assert.ok(handlerStub.called, "Called after event");

  // Test that the trigger doesn't fire when the credit card manager is open.
  handlerStub.resetHistory();
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: "about:preferences#privacy" },
    async browser => {
      await SpecialPowers.spawn(browser, [], async () =>
        (
          await ContentTaskUtils.waitForCondition(
            () => content.document.querySelector("#creditCardAutofill button"),
            "Waiting for credit card manager button"
          )
        )?.click()
      );
      await BrowserTestUtils.waitForCondition(
        () => browser.contentWindow?.gSubDialog?.dialogs.length
      );
      notifyCreditCardSaved();
      await sleepMs(1);
      Assert.ok(
        handlerStub.notCalled,
        "Not called when credit card manager is open"
      );
    }
  );

  formAutofillTrigger.uninit();
  handlerStub.resetHistory();
  notifyCreditCardSaved();
  await sleepMs(1);
  Assert.ok(handlerStub.notCalled, "Not called after uninit");

  sandbox.restore();
  formAutofillTrigger.uninit();
});
