/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-disable @microsoft/sdl/no-insecure-url */

const { MockRegistrar } = ChromeUtils.importESModule(
  "resource://testing-common/MockRegistrar.sys.mjs"
);

const TEST_DOMAIN = "http://example.org";
const TEST_IP = "http://127.0.0.1:8888";
const TEST_PATH = "/browser/comm/mail/base/test/widgets/files/links.html";

const links = new Map([
  ["#this-hash", `${TEST_PATH}#hash`],
  ["#this-nohash", `${TEST_PATH}`],
  [
    "#local-here",
    "/browser/comm/mail/base/test/widgets/files/sampleContent.html",
  ],
  [
    "#local-elsewhere",
    "/browser/comm/mail/components/extensions/test/browser/data/content.html",
  ],
  ["#other-https", `https://example.org${TEST_PATH}`],
  ["#other-port", `http://example.org:8000${TEST_PATH}`],
  ["#other-subdomain", `http://test1.example.org${TEST_PATH}`],
  ["#other-subsubdomain", `http://sub1.test1.example.org${TEST_PATH}`],
  ["#other-domain", `http://mochi.test:8888${TEST_PATH}`],

  ["#svg-new", `https://example.org${TEST_PATH}`],
  ["#svg-blank", `https://example.org${TEST_PATH}`],
  ["#svg-notarget", `https://example.org${TEST_PATH}`],
]);

/** @implements {nsIWebProgressListener} */
const webProgressListener = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIWebProgressListener",
    "nsISupportsWeakReference",
  ]),

  _browser: null,
  _deferred: null,

  onStateChange(webProgress, request, stateFlags) {
    if (
      !(stateFlags & Ci.nsIWebProgressListener.STATE_STOP) ||
      this._browser?.currentURI.spec == "about:blank"
    ) {
      return;
    }

    if (this._deferred) {
      const deferred = this._deferred;
      const url = this._browser.currentURI.spec;
      this.cancelPromise();

      info(`${url} loaded internally`);
      deferred.resolve(url);
    } else {
      this.cancelPromise();
      Assert.ok(false, "unexpected state change");
    }
  },

  onLocationChange(webProgress, request, location, flags) {
    if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_HASHCHANGE)) {
      return;
    }

    if (this._deferred) {
      const deferred = this._deferred;
      const url = this._browser.currentURI.spec;
      this.cancelPromise();

      info(`${url} loaded internally`);
      deferred.resolve(url);
    } else {
      this.cancelPromise();
      Assert.ok(false, "unexpected location change");
    }
  },

  promiseEvent(browser) {
    this._browser = browser;
    browser.webProgress.addProgressListener(
      this,
      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | Ci.nsIWebProgress.NOTIFY_LOCATION
    );

    this._deferred = Promise.withResolvers();
    return this._deferred.promise;
  },

  cancelPromise() {
    this._deferred = null;
    this._browser.removeProgressListener(this);
    this._browser = null;
  },
};

/** @implements {nsIExternalProtocolService} */
const mockExternalProtocolService = {
  QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),

  _deferred: null,

  externalProtocolHandlerExists() {
    return false;
  },

  isExposedProtocol() {
    return true;
  },

  getProtocolHandlerInfo() {
    throw Components.Exception(
      "getProtocolHandlerInfo not implemented",
      Cr.NS_ERROR_NOT_IMPLEMENTED
    );
  },

  getProtocolHandlerInfoFromOS() {
    throw Components.Exception(
      "getProtocolHandlerInfoFromOS not implemented",
      Cr.NS_ERROR_NOT_IMPLEMENTED
    );
  },

  setProtocolHandlerDefaults() {},

  loadURI(aURI) {
    if (this._deferred) {
      const deferred = this._deferred;
      this._deferred = null;
      info(`${aURI.spec} loaded externally`);
      deferred.resolve(aURI.spec);
    } else {
      this.cancelPromise();
      Assert.ok(false, "unexpected call to external protocol service");
    }
  },

  getApplicationDescription() {
    throw Components.Exception(
      "getApplicationDescription not implemented",
      Cr.NS_ERROR_NOT_IMPLEMENTED
    );
  },

  isCurrentAppOSDefaultForProtocol() {
    return true;
  },

  promiseEvent() {
    this._deferred = Promise.withResolvers();
    return this._deferred.promise;
  },

  cancelPromise() {
    this._deferred = null;
  },
};

const mockExternalProtocolServiceCID = MockRegistrar.register(
  "@mozilla.org/uriloader/external-protocol-service;1",
  mockExternalProtocolService
);

// This test deliberately loads content from http:// URLs. For some reason
// upgrading the icon URL to https:// causes it to attempt loading from an
// external server and this makes the test crash.
Services.prefs.setBoolPref(
  "security.mixed_content.upgrade_display_content",
  false
);

registerCleanupFunction(() => {
  const tabmail = document.getElementById("tabmail");
  Assert.equal(tabmail.tabInfo.length, 1);

  while (tabmail.tabInfo.length > 1) {
    tabmail.closeTab(tabmail.tabInfo[1]);
  }

  MockRegistrar.unregister(mockExternalProtocolServiceCID);

  Services.prefs.clearUserPref(
    "security.mixed_content.upgrade_display_content"
  );
});

async function clickOnLink(
  browser,
  selector,
  url,
  pageURL,
  shouldLoadInternally
) {
  if (
    browser.ownerDocument.readyState != "complete" ||
    !browser.currentURI ||
    browser.currentURI?.spec == "about:blank"
  ) {
    await BrowserTestUtils.browserLoaded(browser, false, pageURL);

    // Clear the event queue.
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 250));
  }
  Assert.equal(
    browser.currentURI?.spec,
    pageURL,
    "original URL should be loaded"
  );

  const webProgressPromise = webProgressListener.promiseEvent(browser);
  const externalProtocolPromise = mockExternalProtocolService.promiseEvent();

  info(`scrolling ${selector} into view`);
  await SpecialPowers.spawn(browser, [selector], arg => {
    content.document
      .querySelector(arg)
      .scrollIntoView({ block: "start", behavior: "instant" });
  });
  info(`clicking on ${selector}`);
  await BrowserTestUtils.synthesizeMouseAtCenter(selector, {}, browser);

  await Promise.any([webProgressPromise, externalProtocolPromise]);

  if (selector == "#this-hash") {
    await SpecialPowers.spawn(browser, [], () => {
      const doc = content.document;
      const target = doc.querySelector("#hash");
      const targetRect = target.getBoundingClientRect();
      Assert.less(
        targetRect.bottom,
        doc.documentElement.clientHeight,
        "page did scroll"
      );
    });
  }

  if (shouldLoadInternally) {
    info(`will ensure ${url} loaded internally`);
    Assert.equal(
      await webProgressPromise,
      url,
      `${url} should load internally`
    );
    mockExternalProtocolService.cancelPromise();
  } else {
    info(`will ensure ${url} loaded externally`);
    Assert.equal(
      await externalProtocolPromise,
      url,
      `${url} should load externally`
    );
    webProgressListener.cancelPromise();
  }

  if (browser.currentURI?.spec != pageURL) {
    const promise = webProgressListener.promiseEvent(browser);
    browser.browsingContext.goBack();
    await promise;
    Assert.equal(browser.currentURI?.spec, pageURL, "should have gone back");
  }
}

async function subtest(pagePrePath, group, shouldLoadCB) {
  const tabmail = document.getElementById("tabmail");
  const tab = window.openContentTab(
    `${pagePrePath}${TEST_PATH}`,
    undefined,
    group
  );

  let expectedGroup = group;
  if (group === null) {
    expectedGroup = "browsers";
  } else if (group === undefined) {
    expectedGroup = "single-site";
  }
  Assert.equal(
    tab.browser.getAttribute("messagemanagergroup"),
    expectedGroup,
    "should have the expected messagemanagergroup"
  );

  try {
    for (let [selector, url] of links) {
      if (url.startsWith("/")) {
        url = `${pagePrePath}${url}`;
      }
      await clickOnLink(
        tab.browser,
        selector,
        url,
        `${pagePrePath}${TEST_PATH}`,
        shouldLoadCB(selector)
      );
    }
  } finally {
    tabmail.closeTab(tab);
  }
}

add_task(function testNoGroup() {
  return subtest(
    TEST_DOMAIN,
    undefined,
    selector => selector != "#other-domain" && !selector.startsWith("#svg-")
  );
});

/**
 * Tests the "browsers" message manager group.
 *
 * @see {RelaxedLinkClickHandlerChild}
 */
add_task(function testBrowsersGroup() {
  return subtest(TEST_DOMAIN, null, selector => !selector.startsWith("#svg-"));
});

/**
 * Tests the "single-site" message manager group.
 *
 * @see {LinkClickHandlerChild}
 */
add_task(function testSingleSiteGroup() {
  return subtest(
    TEST_DOMAIN,
    "single-site",
    selector => selector != "#other-domain" && !selector.startsWith("#svg-")
  );
});

/**
 * Tests the "single-page" message manager group.
 *
 * @see {StrictLinkClickHandlerChild}
 */
add_task(function testSinglePageGroup() {
  return subtest(
    TEST_DOMAIN,
    "single-page",
    selector => selector.startsWith("#this") && !selector.startsWith("#svg-")
  );
});

add_task(function testNoGroupWithIP() {
  return subtest(
    TEST_IP,
    undefined,
    selector => selector.startsWith("#this") || selector.startsWith("#local")
  );
});

/**
 * Tests the "browsers" message manager group.
 *
 * @see {RelaxedLinkClickHandlerChild}
 */
add_task(function testBrowsersGroupWithIP() {
  return subtest(TEST_IP, null, selector => !selector.startsWith("#svg-"));
});

add_task(function testSingleSiteGroupWithIP() {
  return subtest(
    TEST_IP,
    "single-site",
    selector => selector.startsWith("#this") || selector.startsWith("#local")
  );
});

add_task(function testSinglePageGroupWithIP() {
  return subtest(TEST_IP, "single-page", selector =>
    selector.startsWith("#this")
  );
});
