/* 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/. */

"use strict";

/* Unit tests for the nsIUrlClassifierSkipListService implementation. */

var httpserver = new HttpServer();

const { NetUtil } = ChromeUtils.importESModule(
  "resource://gre/modules/NetUtil.sys.mjs"
);
const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
);

const FEATURE_STP_PREF = "privacy.trackingprotection.socialtracking.enabled";
const TOP_LEVEL_DOMAIN = "http://www.example.com/";
const TRACKER_DOMAIN = "http://social-tracking.example.org/";

function setupChannel(uri, topUri = TOP_LEVEL_DOMAIN) {
  httpserver.registerPathHandler("/", null);
  httpserver.start(-1);

  let channel = NetUtil.newChannel({
    uri: uri + ":" + httpserver.identity.primaryPort,
    loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal(
      NetUtil.newURI(topUri),
      {}
    ),
    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
  });

  channel
    .QueryInterface(Ci.nsIHttpChannelInternal)
    .setTopWindowURIIfUnknown(Services.io.newURI(topUri));

  return channel;
}

function waitForBeforeBlockEvent(expected, callback) {
  return new Promise(function (resolve) {
    let observer = function observe(aSubject, aTopic, aData) {
      switch (aTopic) {
        case "urlclassifier-before-block-channel":
          let channel = aSubject.QueryInterface(
            Ci.nsIUrlClassifierBlockedChannel
          );
          Assert.equal(
            channel.reason,
            expected.reason,
            "verify blocked reason"
          );
          Assert.equal(
            channel.url,
            expected.url,
            "verify url of blocked channel"
          );

          if (callback) {
            callback(channel);
          }

          service.removeListener(observer);
          resolve(channel);
          break;
      }
    };

    let service = Cc[
      "@mozilla.org/url-classifier/channel-classifier-service;1"
    ].getService(Ci.nsIChannelClassifierService);
    service.addListener(observer);
  });
}

add_task(async function test_block_channel() {
  Services.prefs.setBoolPref(FEATURE_STP_PREF, true);
  await UrlClassifierTestUtils.addTestTrackers();

  let channel = setupChannel(TRACKER_DOMAIN);

  let blockPromise = waitForBeforeBlockEvent(
    {
      reason: Ci.nsIUrlClassifierBlockedChannel.SOCIAL_TRACKING_PROTECTION,
      url: channel.URI.spec,
    },
    null
  );

  let openPromise = new Promise((resolve, reject) => {
    channel.asyncOpen({
      onStartRequest: (request, context) => {},
      onDataAvailable: (request, context, stream, offset, count) => {},
      onStopRequest: (request, status) => {
        dump("status = " + status + "\n");
        if (status == 200) {
          Assert.ok(false, "Should not successfully open the channel");
        } else {
          Assert.equal(
            status,
            Cr.NS_ERROR_SOCIALTRACKING_URI,
            "Should fail to open the channel"
          );
        }
        resolve();
      },
    });
  });

  // wait for block event from url-classifier
  await blockPromise;

  // wait for onStopRequest callback from AsyncOpen
  await openPromise;

  // clean up
  UrlClassifierTestUtils.cleanupTestTrackers();
  Services.prefs.clearUserPref(FEATURE_STP_PREF);
  httpserver.stop();
});

add_task(async function test_unblock_channel() {
  Services.prefs.setBoolPref(FEATURE_STP_PREF, true);
  //Services.prefs.setBoolPref("network.dns.native-is-localhost", true);

  await UrlClassifierTestUtils.addTestTrackers();

  let channel = setupChannel(TRACKER_DOMAIN);

  let blockPromise = waitForBeforeBlockEvent(
    {
      reason: Ci.nsIUrlClassifierBlockedChannel.SOCIAL_TRACKING_PROTECTION,
      url: channel.URI.spec,
    },
    ch => {
      ch.replace();
    }
  );

  let openPromise = new Promise((resolve, reject) => {
    channel.asyncOpen({
      onStartRequest: (request, context) => {},
      onDataAvailable: (request, context, stream, offset, count) => {},
      onStopRequest: (request, status) => {
        if (status == Cr.NS_ERROR_SOCIALTRACKING_URI) {
          Assert.ok(false, "Classifier should not cancel this channel");
        } else {
          // This request is supposed to fail, but we need to ensure it
          // is not canceled by url-classifier
          Assert.equal(
            status,
            Cr.NS_ERROR_UNKNOWN_HOST,
            "Not cancel by classifier"
          );
        }
        resolve();
      },
    });
  });

  // wait for block event from url-classifier
  await blockPromise;

  // wait for onStopRequest callback from AsyncOpen
  await openPromise;

  // clean up
  UrlClassifierTestUtils.cleanupTestTrackers();
  Services.prefs.clearUserPref(FEATURE_STP_PREF);
  httpserver.stop();
});

add_task(async function test_allow_channel() {
  Services.prefs.setBoolPref(FEATURE_STP_PREF, true);
  //Services.prefs.setBoolPref("network.dns.native-is-localhost", true);

  await UrlClassifierTestUtils.addTestTrackers();

  let channel = setupChannel(TRACKER_DOMAIN);

  let blockPromise = waitForBeforeBlockEvent(
    {
      reason: Ci.nsIUrlClassifierBlockedChannel.SOCIAL_TRACKING_PROTECTION,
      url: channel.URI.spec,
    },
    ch => {
      ch.allow();
    }
  );

  let openPromise = new Promise((resolve, reject) => {
    channel.asyncOpen({
      onStartRequest: (request, context) => {},
      onDataAvailable: (request, context, stream, offset, count) => {},
      onStopRequest: (request, status) => {
        if (status == Cr.NS_ERROR_SOCIALTRACKING_URI) {
          Assert.ok(false, "Classifier should not cancel this channel");
        } else {
          // This request is supposed to fail, but we need to ensure it
          // is not canceled by url-classifier
          Assert.equal(
            status,
            Cr.NS_ERROR_UNKNOWN_HOST,
            "Not cancel by classifier"
          );
        }
        resolve();
      },
    });
  });

  // wait for block event from url-classifier
  await blockPromise;

  // wait for onStopRequest callback from AsyncOpen
  await openPromise;

  // clean up
  UrlClassifierTestUtils.cleanupTestTrackers();
  Services.prefs.clearUserPref(FEATURE_STP_PREF);
  httpserver.stop();
});
