/* 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";

// The URL will guarantee to trigger a real-time request.
const TEST_URL = "https://www.example.com/";

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

function waitForSimulationResult() {
  return new Promise(resolve => {
    function observer(subject, topic, data) {
      Services.obs.removeObserver(
        observer,
        "urlclassifier-realtime-simulation-result"
      );
      let [wouldSendRequest, requestBytes, responseBytes] = data.split(",");
      resolve({
        wouldSendRequest: wouldSendRequest === "1",
        requestBytes: parseInt(requestBytes, 10),
        responseBytes: parseInt(responseBytes, 10),
      });
    }
    Services.obs.addObserver(
      observer,
      "urlclassifier-realtime-simulation-result"
    );
  });
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.safebrowsing.realTime.enabled", true],
      ["browser.safebrowsing.realTime.debug", true],
      ["browser.safebrowsing.globalCache.enabled", true],
      ["browser.safebrowsing.realTime.simulation.enabled", true],
      ["browser.safebrowsing.realTime.simulation.cacheTTLSec", 1],
    ],
  });

  registerCleanupFunction(async () => {
    UrlClassifierTestUtils.cleanRealTimeSimulatorCache();
    Services.prefs.clearUserPref(
      "privacy.trackingprotection.allow_list.hasUserInteractedWithETPSettings"
    );
  });
});

// Test that the simulator sends a notification when processing a URL.
add_task(async function test_simulation_notification() {
  info("Testing simulation notification");

  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  // Set the hit probability to 0.0 so we always get a cache miss.
  await SpecialPowers.pushPrefEnv({
    set: [["browser.safebrowsing.realTime.simulation.hitProbability", 0]],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  let resultPromise = waitForSimulationResult();

  let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);

  let result = await resultPromise;

  ok(result.wouldSendRequest, "Should indicate a request would be sent");
  Assert.greater(
    result.requestBytes,
    0,
    "Request bytes should be greater than 0"
  );
  is(result.responseBytes, 5, "Response bytes should be 5 (no hits)");

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
});

// Test that the telemetry is properly recorded.
add_task(async function test_simulation_telemetry() {
  info("Testing simulation telemetry recording");

  Services.fog.testResetFOG();
  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.contentblocking.category", "standard"],
      ["browser.safebrowsing.realTime.simulation.hitProbability", 0],
    ],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  let resultPromise = waitForSimulationResult();
  let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  let result = await resultPromise;

  ok(result.wouldSendRequest, "Request should be sent");

  // Verify Global Cache telemetry.
  is(
    Glean.urlclassifier.globalCacheMiss.standard_normal.testGetValue(),
    1,
    "Global cache miss should be recorded for standard_normal"
  );
  is(
    Glean.urlclassifier.globalCacheHit.standard_normal.testGetValue(),
    null,
    "No global cache hit should be recorded"
  );

  // Verify simulation telemetry.
  is(
    Glean.urlclassifier.realtimeSimulationRequestCount.standard_normal.testGetValue(),
    1,
    "Request count should be 1 for standard_normal"
  );
  Assert.greater(
    Glean.urlclassifier.realtimeSimulationRequestSize.standard_normal.testGetValue(),
    0,
    "Request size should be greater than 0"
  );
  is(
    Glean.urlclassifier.realtimeSimulationResponseSize.standard_normal.testGetValue(),
    5,
    "Response size should be 5 (no hits)"
  );

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
  await SpecialPowers.popPrefEnv();
});

// Test that globalCacheHit telemetry is recorded when the URL is in the cache.
add_task(async function test_global_cache_hit_telemetry() {
  info("Testing global cache hit telemetry");

  Services.fog.testResetFOG();

  await SpecialPowers.pushPrefEnv({
    set: [["browser.contentblocking.category", "standard"]],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  let notificationPromise = new Promise(resolve => {
    function observer(subject, topic, data) {
      Services.obs.removeObserver(observer, "urlclassifier-globalcache-result");
      resolve(data);
    }
    Services.obs.addObserver(observer, "urlclassifier-globalcache-result");
  });

  let tab = BrowserTestUtils.addTab(
    gBrowser,
    "https://globalcache-test.example.com/"
  );
  let result = await notificationPromise;

  is(result, "hit", "Should get a global cache hit");

  // Verify Global Cache hit telemetry.
  is(
    Glean.urlclassifier.globalCacheHit.standard_normal.testGetValue(),
    1,
    "Global cache hit should be recorded for standard_normal"
  );
  is(
    Glean.urlclassifier.globalCacheMiss.standard_normal.testGetValue(),
    null,
    "No global cache miss should be recorded"
  );

  // Verify no simulation telemetry is recorded for a cache hit.
  is(
    Glean.urlclassifier.realtimeSimulationRequestCount.standard_normal.testGetValue(),
    null,
    "No simulation request count for global cache hit"
  );

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
  await SpecialPowers.popPrefEnv();
});

// Test telemetry with strict ETP mode and private window.
add_task(async function test_simulation_telemetry_strict_private() {
  info("Testing simulation telemetry with strict ETP in private window");

  Services.fog.testResetFOG();
  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.contentblocking.category", "strict"],
      ["browser.safebrowsing.realTime.simulation.hitProbability", 0],
    ],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  let privateWin = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });

  let resultPromise = waitForSimulationResult();
  let tab = BrowserTestUtils.addTab(privateWin.gBrowser, TEST_URL);
  let result = await resultPromise;

  ok(result.wouldSendRequest, "Request should be sent");

  // Verify telemetry is recorded under strict_private.
  is(
    Glean.urlclassifier.globalCacheMiss.strict_private.testGetValue(),
    1,
    "Global cache miss should be recorded for strict_private"
  );
  is(
    Glean.urlclassifier.realtimeSimulationRequestCount.strict_private.testGetValue(),
    1,
    "Request count should be 1 for strict_private"
  );
  Assert.greater(
    Glean.urlclassifier.realtimeSimulationRequestSize.strict_private.testGetValue(),
    0,
    "Request size should be greater than 0 for strict_private"
  );

  // Verify no telemetry recorded for standard_normal.
  is(
    Glean.urlclassifier.realtimeSimulationRequestCount.standard_normal.testGetValue(),
    null,
    "No request count should be recorded for standard_normal"
  );

  await BrowserTestUtils.removeTab(tab);
  await BrowserTestUtils.closeWindow(privateWin);
  UrlClassifierTestUtils.cleanupTestV5Entry();
  await SpecialPowers.popPrefEnv();
});

// Test that the simulator caches results and doesn't send duplicate requests.
add_task(async function test_simulation_cache() {
  info("Testing simulation cache behavior");

  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  await UrlClassifierTestUtils.addTestV5Entry();

  // Set hit probability to 1000000 (100%) so we always get a cache entry.
  await SpecialPowers.pushPrefEnv({
    set: [["browser.safebrowsing.realTime.simulation.hitProbability", 1000000]],
  });

  // First request should be sent.
  let resultPromise = waitForSimulationResult();
  let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  let result = await resultPromise;

  ok(result.wouldSendRequest, "First request should be sent");
  Assert.greater(
    result.requestBytes,
    0,
    "Request bytes should be greater than 0"
  );
  Assert.greater(
    result.responseBytes,
    5,
    "Response bytes should include hit data"
  );

  await BrowserTestUtils.removeTab(tab);

  // Second request to the same URL should hit the cache.
  resultPromise = waitForSimulationResult();
  tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  result = await resultPromise;

  ok(!result.wouldSendRequest, "Second request should hit cache");
  is(result.requestBytes, 0, "No request bytes for cache hit");
  is(result.responseBytes, 0, "No response bytes for cache hit");

  await BrowserTestUtils.removeTab(tab);

  // Force-expire all cache entries.
  UrlClassifierTestUtils.expireRealTimeSimulatorCache();

  // Third request should be sent again after cache expiry.
  resultPromise = waitForSimulationResult();
  tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  result = await resultPromise;

  ok(
    result.wouldSendRequest,
    "Third request should be sent after cache expiry"
  );
  Assert.greater(
    result.requestBytes,
    0,
    "Request bytes should be greater than 0 after expiry"
  );
  Assert.greater(
    result.responseBytes,
    5,
    "Response bytes should include hit data after expiry"
  );

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
});

// Test that the negative cache skips requests for previously-missed full hashes.
add_task(async function test_negative_cache() {
  info("Testing negative cache behavior");

  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.safebrowsing.realTime.simulation.negativeCacheEnabled", true],
      ["browser.safebrowsing.realTime.simulation.negativeCacheTTLSec", 1],
      ["browser.safebrowsing.realTime.simulation.hitProbability", 0],
    ],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  // First request should be sent (nothing in negative cache yet).
  let resultPromise = waitForSimulationResult();
  let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  let result = await resultPromise;

  ok(result.wouldSendRequest, "First request should be sent");
  Assert.greater(
    result.requestBytes,
    0,
    "Request bytes should be greater than 0"
  );

  await BrowserTestUtils.removeTab(tab);

  // Second request should be skipped due to negative cache.
  resultPromise = waitForSimulationResult();
  tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  result = await resultPromise;

  ok(
    !result.wouldSendRequest,
    "Second request should be skipped by negative cache"
  );
  is(result.requestBytes, 0, "No request bytes for negative cache hit");
  is(result.responseBytes, 0, "No response bytes for negative cache hit");

  await BrowserTestUtils.removeTab(tab);

  // Force-expire all cache entries.
  UrlClassifierTestUtils.expireRealTimeSimulatorCache();

  // Third request should be sent again after negative cache expiry.
  resultPromise = waitForSimulationResult();
  tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  result = await resultPromise;

  ok(
    result.wouldSendRequest,
    "Third request should be sent after negative cache expiry"
  );
  Assert.greater(
    result.requestBytes,
    0,
    "Request bytes should be greater than 0 after expiry"
  );

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
  await SpecialPowers.popPrefEnv();
});

// Test that the negative cache is not used when the pref is disabled.
add_task(async function test_negative_cache_disabled() {
  info("Testing negative cache is not used when disabled");

  UrlClassifierTestUtils.cleanRealTimeSimulatorCache();

  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.safebrowsing.realTime.simulation.negativeCacheEnabled", false],
      ["browser.safebrowsing.realTime.simulation.hitProbability", 0],
    ],
  });

  await UrlClassifierTestUtils.addTestV5Entry();

  // First request should be sent.
  let resultPromise = waitForSimulationResult();
  let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  let result = await resultPromise;

  ok(result.wouldSendRequest, "First request should be sent");

  await BrowserTestUtils.removeTab(tab);

  // Second request should also be sent (negative cache disabled).
  resultPromise = waitForSimulationResult();
  tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
  result = await resultPromise;

  ok(
    result.wouldSendRequest,
    "Second request should still be sent when negative cache is disabled"
  );

  await BrowserTestUtils.removeTab(tab);
  UrlClassifierTestUtils.cleanupTestV5Entry();
  await SpecialPowers.popPrefEnv();
});
