/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

requestLongerTimeout(4);

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

ChromeUtils.defineESModuleGetters(this, {
  DoHConfigController: "moz-src:///toolkit/components/doh/DoHConfig.sys.mjs",
  DoHController: "moz-src:///toolkit/components/doh/DoHController.sys.mjs",
  DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs",
});

const { MockRegistrar } = ChromeUtils.importESModule(
  "resource://testing-common/MockRegistrar.sys.mjs"
);
const gDNSOverride = Cc[
  "@mozilla.org/network/native-dns-override;1"
].getService(Ci.nsINativeDNSResolverOverride);

const TRR_MODE_PREF = "network.trr.mode";
const TRR_URI_PREF = "network.trr.uri";
const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri";
const ROLLOUT_ENABLED_PREF = "doh-rollout.enabled";
const ROLLOUT_SELF_ENABLED_PREF = "doh-rollout.self-enabled";
const HEURISTICS_DISABLED_PREF = "doh-rollout.disable-heuristics";
const FIRST_RESOLVER_VALUE = DoHTestUtils.providers[0].uri;
const SECOND_RESOLVER_VALUE = DoHTestUtils.providers[1].uri;
const DEFAULT_RESOLVER_VALUE = FIRST_RESOLVER_VALUE;

const defaultPrefValues = Object.freeze({
  [TRR_MODE_PREF]: 0,
  [TRR_CUSTOM_URI_PREF]: "",
});

// See bug 1741554. This test should not actually try to create a connection to
// the real DoH endpoint. But a background request could do that while the test
// is in progress, before we've actually disabled TRR, and would cause a crash
// due to connecting to a non-local IP.
// To prevent that we override the IP to a local address.
gDNSOverride.addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1");

async function clearEvents() {
  Services.telemetry.clearEvents();
  await TestUtils.waitForCondition(() => {
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_ALL_CHANNELS,
      true
    ).parent;
    return !events || !events.length;
  });
}

async function getEvent(filter1, filter2) {
  let event = await TestUtils.waitForCondition(() => {
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_ALL_CHANNELS,
      true
    ).parent;
    return events?.find(e => e[1] == filter1 && e[2] == filter2);
  }, "recorded telemetry for the load");
  event.shift();
  return event;
}

// Mock parental controls service in order to enable it
let parentalControlsService = {
  parentalControlsEnabled: true,
  QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
};
let mockParentalControlsServiceCid = undefined;

async function setMockParentalControlEnabled(aEnabled) {
  parentalControlsService.parentalControlsEnabled = aEnabled;
}

async function resetPrefs() {
  await DoHTestUtils.resetRemoteSettingsConfig();
  await DoHController._uninit();
  Services.prefs.clearUserPref(TRR_MODE_PREF);
  Services.prefs.clearUserPref(TRR_URI_PREF);
  Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF);
  Services.prefs.getChildList("doh-rollout.").forEach(pref => {
    Services.prefs.clearUserPref(pref);
  });
  // Clear out any telemetry events generated by DoHController so that we don't
  // confuse tests running after this one that are looking at those.
  Services.telemetry.clearEvents();
  await DoHController.init();
}
Services.prefs.setStringPref("network.trr.confirmationNS", "skip");

registerCleanupFunction(async () => {
  await resetPrefs();
  Services.prefs.clearUserPref("network.trr.confirmationNS");

  if (mockParentalControlsServiceCid != undefined) {
    MockRegistrar.unregister(mockParentalControlsServiceCid);
    mockParentalControlsServiceCid = undefined;
    Services.dns.reloadParentalControlEnabled();
  }
});

add_setup(async function setup() {
  mockParentalControlsServiceCid = MockRegistrar.register(
    "@mozilla.org/parental-controls-service;1",
    parentalControlsService
  );
  Services.dns.reloadParentalControlEnabled();

  await SpecialPowers.pushPrefEnv({
    set: [["toolkit.telemetry.testing.overrideProductsCheck", true]],
  });

  await DoHTestUtils.resetRemoteSettingsConfig();

  gDNSOverride.addIPOverride("use-application-dns.net.", "4.1.1.1");

  setMockParentalControlEnabled(false);
});

function waitForPrefObserver(name) {
  return new Promise(resolve => {
    const observer = {
      observe(aSubject, aTopic, aData) {
        if (aData == name) {
          Services.prefs.removeObserver(name, observer);
          resolve();
        }
      },
    };
    Services.prefs.addObserver(name, observer);
  });
}

add_task(async function testParentalControls() {
  async function withConfiguration(configuration, fn) {
    info("testParentalControls");

    await resetPrefs();
    Services.prefs.setIntPref(TRR_MODE_PREF, configuration.trr_mode);
    await setMockParentalControlEnabled(configuration.parentalControlsState);

    await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
    let doc = gBrowser.selectedBrowser.contentDocument;
    let statusElement = doc.getElementById("dohStatus");

    await TestUtils.waitForCondition(() => {
      return (
        document.l10n.getAttributes(statusElement).args.status ==
        configuration.wait_for_doh_status
      );
    });

    await fn({
      statusElement,
    });

    gBrowser.removeCurrentTab();
    await setMockParentalControlEnabled(false);
  }

  info("Check parental controls disabled, TRR off");
  await withConfiguration(
    {
      parentalControlsState: false,
      trr_mode: 0,
      wait_for_doh_status: "Off",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Off",
        "expecting status off"
      );
    }
  );

  info("Check parental controls enabled, TRR off");
  await withConfiguration(
    {
      parentalControlsState: true,
      trr_mode: 0,
      wait_for_doh_status: "Off",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Off",
        "expecting status off"
      );
    }
  );

  // Enable the rollout.
  await DoHTestUtils.loadRemoteSettingsConfig({
    providers: "example",
    rolloutEnabled: true,
    steeringEnabled: false,
    steeringProviders: "",
    autoDefaultEnabled: false,
    autoDefaultProviders: "",
    id: "global",
  });

  info("Check parental controls disabled, TRR first");
  await withConfiguration(
    {
      parentalControlsState: false,
      trr_mode: 2,
      wait_for_doh_status: "Active",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Active",
        "expecting status active"
      );
    }
  );

  info("Check parental controls enabled, TRR first");
  await withConfiguration(
    {
      parentalControlsState: true,
      trr_mode: 2,
      wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Not active (TRR_PARENTAL_CONTROL)",
        "expecting status not active"
      );
    }
  );

  info("Check parental controls disabled, TRR only");
  await withConfiguration(
    {
      parentalControlsState: false,
      trr_mode: 3,
      wait_for_doh_status: "Active",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Active",
        "expecting status active"
      );
    }
  );

  info("Check parental controls enabled, TRR only");
  await withConfiguration(
    {
      parentalControlsState: true,
      trr_mode: 3,
      wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)",
    },
    async res => {
      is(
        document.l10n.getAttributes(res.statusElement).args.status,
        "Not active (TRR_PARENTAL_CONTROL)",
        "expecting status not active"
      );
    }
  );

  await resetPrefs();
});

async function testWithProperties(props, startTime) {
  info(
    Date.now() -
      startTime +
      ": testWithProperties: testing with " +
      JSON.stringify(props)
  );

  // There are two different signals that the DoHController is ready, depending
  // on the config being tested. If we're setting the TRR mode pref, we should
  // expect the disable-heuristics pref to be set as the signal. Else, we can
  // expect the self-enabled pref as the signal.
  let rolloutReadyPromise;
  if (props.hasOwnProperty(TRR_MODE_PREF)) {
    if (
      [2, 3, 5].includes(props[TRR_MODE_PREF]) &&
      props.hasOwnProperty(ROLLOUT_ENABLED_PREF)
    ) {
      // Only initialize the promise if we're going to enable the rollout -
      // otherwise we will never await it, which could cause a leak if it doesn't
      // end up resolving.
      rolloutReadyPromise = waitForPrefObserver(HEURISTICS_DISABLED_PREF);
    }
    Services.prefs.setIntPref(TRR_MODE_PREF, props[TRR_MODE_PREF]);
  }
  if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) {
    if (!rolloutReadyPromise) {
      rolloutReadyPromise = waitForPrefObserver(ROLLOUT_SELF_ENABLED_PREF);
    }
    Services.prefs.setBoolPref(
      ROLLOUT_ENABLED_PREF,
      props[ROLLOUT_ENABLED_PREF]
    );
    await rolloutReadyPromise;
  }
  if (props.hasOwnProperty(TRR_CUSTOM_URI_PREF)) {
    Services.prefs.setStringPref(
      TRR_CUSTOM_URI_PREF,
      props[TRR_CUSTOM_URI_PREF]
    );
  }
  if (props.hasOwnProperty(TRR_URI_PREF)) {
    Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]);
  }

  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
  let doc = gBrowser.selectedBrowser.contentDocument;

  info(Date.now() - startTime + ": testWithProperties: tab now open");
  let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");
  let uriTextbox = doc.getElementById("dohEnabledInputField");
  let resolverMenulist = doc.getElementById("dohStrictResolverChoices");
  let modePrefChangedPromise;
  let uriPrefChangedPromise;
  let disableHeuristicsPrefChangedPromise;

  modeRadioGroup.scrollIntoView();

  if (props.hasOwnProperty("expectedSelectedIndex")) {
    await TestUtils.waitForCondition(
      () => modeRadioGroup.selectedIndex === props.expectedSelectedIndex
    );
    is(
      modeRadioGroup.selectedIndex,
      props.expectedSelectedIndex,
      "dohCategoryRadioGroup has expected selected index"
    );
  }
  if (props.hasOwnProperty("expectedUriValue")) {
    await TestUtils.waitForCondition(
      () => uriTextbox.value === props.expectedUriValue
    );
    is(
      uriTextbox.value,
      props.expectedUriValue,
      "URI textbox has expected value"
    );
  }
  if (props.hasOwnProperty("expectedResolverListValue")) {
    await TestUtils.waitForCondition(
      () => resolverMenulist.value === props.expectedResolverListValue
    );
    is(
      resolverMenulist.value,
      props.expectedResolverListValue,
      "resolver menulist has expected value"
    );
  }

  if (props.clickMode) {
    await clearEvents();
    info(
      Date.now() -
        startTime +
        ": testWithProperties: clickMode, waiting for the pref observer"
    );
    modePrefChangedPromise = waitForPrefObserver(TRR_MODE_PREF);
    if (props.hasOwnProperty("expectedDisabledHeuristics")) {
      disableHeuristicsPrefChangedPromise = waitForPrefObserver(
        HEURISTICS_DISABLED_PREF
      );
    }
    info(
      Date.now() - startTime + ": testWithProperties: clickMode, pref changed"
    );
    let option = doc.getElementById(props.clickMode);
    option.scrollIntoView();
    let win = doc.ownerGlobal;
    EventUtils.synthesizeMouseAtCenter(option, {}, win);
    info(
      `${Date.now() - startTime} : testWithProperties: clickMode=${
        props.clickMode
      }, mouse click synthesized`
    );
    let clickEvent = await getEvent("security.doh.settings", "mode_changed");
    Assert.deepEqual(clickEvent, [
      "security.doh.settings",
      "mode_changed",
      "button",
      props.clickMode,
    ]);
  }
  if (props.hasOwnProperty("selectResolver")) {
    await clearEvents();
    info(
      Date.now() -
        startTime +
        ": testWithProperties: selectResolver, creating change event"
    );
    resolverMenulist.focus();
    resolverMenulist.value = props.selectResolver;
    resolverMenulist.dispatchEvent(new Event("input", { bubbles: true }));
    resolverMenulist.dispatchEvent(new Event("command", { bubbles: true }));
    info(
      Date.now() -
        startTime +
        ": testWithProperties: selectResolver, item value set and events dispatched"
    );
    let choiceEvent = await getEvent(
      "security.doh.settings",
      "provider_choice"
    );
    Assert.deepEqual(choiceEvent, [
      "security.doh.settings",
      "provider_choice",
      "value",
      props.selectResolver,
    ]);
  }
  if (props.hasOwnProperty("inputUriKeys")) {
    info(
      Date.now() -
        startTime +
        ": testWithProperties: inputUriKeys, waiting for the pref observer"
    );
    uriPrefChangedPromise = waitForPrefObserver(TRR_CUSTOM_URI_PREF);
    info(
      Date.now() -
        startTime +
        ": testWithProperties: inputUriKeys, pref changed, now enter the new value"
    );
    let win = doc.ownerGlobal;
    uriTextbox.focus();
    uriTextbox.value = props.inputUriKeys;
    uriTextbox.dispatchEvent(new win.Event("input", { bubbles: true }));
    uriTextbox.dispatchEvent(new win.Event("change", { bubbles: true }));
    info(
      Date.now() -
        startTime +
        ": testWithProperties: inputUriKeys, input and change events dispatched"
    );
  }

  info(
    Date.now() -
      startTime +
      ": testWithProperties: waiting for any of uri and mode prefs to change"
  );
  await Promise.all([
    uriPrefChangedPromise,
    modePrefChangedPromise,
    disableHeuristicsPrefChangedPromise,
  ]);
  info(Date.now() - startTime + ": testWithProperties: prefs changed");

  if (props.hasOwnProperty("expectedFinalUriPref")) {
    if (props.expectedFinalUriPref) {
      let uriPref = Services.prefs.getStringPref(TRR_URI_PREF);
      is(
        uriPref,
        props.expectedFinalUriPref,
        "uri pref ended up with the expected value"
      );
    } else {
      ok(
        !Services.prefs.prefHasUserValue(TRR_URI_PREF),
        `uri pref ended up with the expected value (unset) got ${Services.prefs.getStringPref(
          TRR_URI_PREF
        )}`
      );
    }
  }

  if (props.hasOwnProperty("expectedModePref")) {
    let modePref = Services.prefs.getIntPref(TRR_MODE_PREF);
    is(
      modePref,
      props.expectedModePref,
      "mode pref ended up with the expected value"
    );
  }

  if (props.hasOwnProperty("expectedDisabledHeuristics")) {
    let disabledHeuristicsPref = Services.prefs.getBoolPref(
      HEURISTICS_DISABLED_PREF
    );
    is(
      disabledHeuristicsPref,
      props.expectedDisabledHeuristics,
      "disable-heuristics pref ended up with the expected value"
    );
  }

  if (props.hasOwnProperty("expectedFinalCustomUriPref")) {
    let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF);
    is(
      customUriPref,
      props.expectedFinalCustomUriPref,
      "custom_uri pref ended up with the expected value"
    );
  }

  if (props.hasOwnProperty("expectedModeValue")) {
    let modeValue = Services.prefs.getIntPref(TRR_MODE_PREF);
    is(modeValue, props.expectedModeValue, "mode pref has expected value");
  }

  if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) {
    Services.prefs.clearUserPref(ROLLOUT_ENABLED_PREF);
  }

  gBrowser.removeCurrentTab();
  info(Date.now() - startTime + ": testWithProperties: fin");
}

add_task(async function default_values() {
  let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF);
  let uriPrefHasUserValue = Services.prefs.prefHasUserValue(TRR_URI_PREF);
  let modePref = Services.prefs.getIntPref(TRR_MODE_PREF);
  is(
    modePref,
    defaultPrefValues[TRR_MODE_PREF],
    `Actual value of ${TRR_MODE_PREF} matches expected default value`
  );
  ok(
    !uriPrefHasUserValue,
    `Actual value of ${TRR_URI_PREF} matches expected default value (unset)`
  );
  is(
    customUriPref,
    defaultPrefValues[TRR_CUSTOM_URI_PREF],
    `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value`
  );
});

const DEFAULT_OPTION_INDEX = 0;
const ENABLED_OPTION_INDEX = 1;
const STRICT_OPTION_INDEX = 2;
const OFF_OPTION_INDEX = 3;

let testVariations = [
  // verify state with defaults
  {
    name: "default",
    expectedModePref: 0,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
    expectedUriValue: "",
  },

  // verify each of the modes maps to the correct checked state
  {
    name: "mode 0",
    [TRR_MODE_PREF]: 0,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
  },
  {
    name: "mode 1",
    [TRR_MODE_PREF]: 1,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "mode 2",
    [TRR_MODE_PREF]: 2,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
  },
  {
    name: "mode 2 and match default uri",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: "",
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
    expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
  },
  {
    name: "mode 3",
    [TRR_MODE_PREF]: 3,
    expectedSelectedIndex: STRICT_OPTION_INDEX,
  },
  {
    name: "mode 3 and match default uri",
    [TRR_URI_PREF]: "",
    [TRR_MODE_PREF]: 3,
    expectedSelectedIndex: STRICT_OPTION_INDEX,
    expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
  },
  {
    name: "mode 4",
    [TRR_MODE_PREF]: 4,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "mode 5",
    [TRR_MODE_PREF]: 5,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  // verify an out of bounds mode value maps to the correct checked state
  {
    name: "mode out-of-bounds",
    [TRR_MODE_PREF]: 77,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "mode out-of-bounds-negative",
    [TRR_MODE_PREF]: -1,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  // Changing mode 0 to mode 2 and 3
  {
    name: "back to mode 0",
    [TRR_MODE_PREF]: 0,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
  },
  {
    name: "mode 2 after mode 0",
    [TRR_MODE_PREF]: 2,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  {
    name: "back to mode 0_2",
    [TRR_MODE_PREF]: 0,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
  },
  {
    name: "mode 3 after mode 0",
    [TRR_MODE_PREF]: 3,
    expectedSelectedIndex: STRICT_OPTION_INDEX,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  // verify the final URI of changing mode 5 to mode 2 and 3
  {
    name: "back to mode 5",
    [TRR_MODE_PREF]: 5,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "mode 2 after mode 5",
    [TRR_MODE_PREF]: 2,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  {
    name: "back to mode 5_2",
    [TRR_MODE_PREF]: 5,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "mode 3 after mode 5",
    [TRR_MODE_PREF]: 3,
    expectedSelectedIndex: STRICT_OPTION_INDEX,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  // verify automatic heuristics states
  {
    name: "heuristics on and mode unset",
    [TRR_MODE_PREF]: 0,
    [ROLLOUT_ENABLED_PREF]: true,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
  },
  {
    name: "heuristics on and mode set to 2",
    [TRR_MODE_PREF]: 2,
    [ROLLOUT_ENABLED_PREF]: true,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
  },
  {
    name: "heuristics on but disabled, mode unset",
    [TRR_MODE_PREF]: 5,
    [ROLLOUT_ENABLED_PREF]: true,
    expectedSelectedIndex: OFF_OPTION_INDEX,
  },
  {
    name: "heuristics on but disabled, mode set to 2",
    [TRR_MODE_PREF]: 2,
    [ROLLOUT_ENABLED_PREF]: true,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
  },

  // verify picking each radio button option gives the right outcomes
  {
    name: "toggle mode on",
    clickMode: "dohEnabledRadio",
    expectedModeValue: 2,
    expectedUriValue: "",
  },
  // TRR_URI_PREF should match the expectedFinalUriPref based on the changes made in Bug 1861285
  {
    name: "toggle mode on and auto default uri",
    [TRR_URI_PREF]: "https://stub.com",
    clickMode: "dohEnabledRadio",
    expectedModeValue: 2,
    expectedUriValue: "",
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  {
    name: "toggle mode off",
    [TRR_MODE_PREF]: 2,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
    clickMode: "dohOffRadio",
    expectedModePref: 5,
  },
  {
    name: "toggle mode off when on due to heuristics",
    [TRR_MODE_PREF]: 0,
    [ROLLOUT_ENABLED_PREF]: true,
    expectedSelectedIndex: DEFAULT_OPTION_INDEX,
    clickMode: "dohOffRadio",
    expectedModePref: 5,
    expectedDisabledHeuristics: true,
  },
  // Test selecting non-default, non-custom TRR provider, NextDNS.
  {
    name: "Select NextDNS as TRR provider",
    [TRR_MODE_PREF]: 2,
    selectResolver: SECOND_RESOLVER_VALUE,
    expectedFinalUriPref: SECOND_RESOLVER_VALUE,
  },
  // Test selecting non-default, non-custom TRR provider, NextDNS,
  // with DoH not enabled. The provider selection should stick.
  {
    name: "Select NextDNS as TRR provider in mode 0",
    [TRR_MODE_PREF]: 0,
    selectResolver: SECOND_RESOLVER_VALUE,
    expectedFinalUriPref: SECOND_RESOLVER_VALUE,
  },
  {
    name: "return to default from NextDNS",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: SECOND_RESOLVER_VALUE,
    expectedResolverListValue: SECOND_RESOLVER_VALUE,
    selectResolver: DEFAULT_RESOLVER_VALUE,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  // Attempt to select NextDNS with DoH not enabled
  // from mode 5 to 3
  {
    name: "Select NextDNS as TRR provider in mode 5",
    [TRR_MODE_PREF]: 5,
    selectResolver: SECOND_RESOLVER_VALUE,
    expectedFinalUriPref: SECOND_RESOLVER_VALUE,
  },
  {
    name: "return to default from NextDNS_2",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: SECOND_RESOLVER_VALUE,
    expectedResolverListValue: SECOND_RESOLVER_VALUE,
    selectResolver: DEFAULT_RESOLVER_VALUE,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  {
    name: "Select NextDNS as TRR provider in mode 5_2",
    [TRR_MODE_PREF]: 5,
    selectResolver: SECOND_RESOLVER_VALUE,
    expectedFinalUriPref: SECOND_RESOLVER_VALUE,
  },
  {
    name: "return to default from NextDNS_3",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: SECOND_RESOLVER_VALUE,
    expectedResolverListValue: SECOND_RESOLVER_VALUE,
    selectResolver: DEFAULT_RESOLVER_VALUE,
    expectedFinalUriPref: FIRST_RESOLVER_VALUE,
  },
  // test that selecting Custom, when we have a TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF
  {
    name: "select custom with existing custom_uri pref value",
    [TRR_MODE_PREF]: 2,
    [TRR_CUSTOM_URI_PREF]: "https://example.com",
    expectedModeValue: 2,
    expectedSelectedIndex: ENABLED_OPTION_INDEX,
    selectResolver: "custom",
    expectedUriValue: "https://example.com",
    expectedFinalUriPref: "https://example.com",
    expectedFinalCustomUriPref: "https://example.com",
  },
  {
    name: "select custom and enter new custom_uri pref value",
    [TRR_URI_PREF]: "",
    [TRR_CUSTOM_URI_PREF]: "",
    clickMode: "dohEnabledRadio",
    selectResolver: "custom",
    inputUriKeys: "https://custom.com",
    expectedModePref: 2,
    expectedFinalUriPref: "https://custom.com",
    expectedFinalCustomUriPref: "https://custom.com",
  },

  {
    name: "return to default from custom",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: "https://example.com",
    [TRR_CUSTOM_URI_PREF]: "https://custom.com",
    expectedUriValue: "https://example.com",
    expectedResolverListValue: "custom",
    selectResolver: DEFAULT_RESOLVER_VALUE,
    expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
    expectedFinalCustomUriPref: "https://example.com",
  },
  {
    name: "clear the custom uri",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: "https://example.com",
    [TRR_CUSTOM_URI_PREF]: "https://example.com",
    expectedUriValue: "https://example.com",
    expectedResolverListValue: "custom",
    inputUriKeys: "",
    expectedFinalUriPref: " ",
    expectedFinalCustomUriPref: "",
  },
  {
    name: "empty default resolver list",
    [TRR_MODE_PREF]: 2,
    [TRR_URI_PREF]: "https://example.com",
    [TRR_CUSTOM_URI_PREF]: "",
    expectedUriValue: "https://example.com",
    expectedResolverListValue: "custom",
    expectedFinalUriPref: "https://example.com",
    expectedFinalCustomUriPref: "https://example.com",
  },
];

for (let props of testVariations) {
  add_task(async function testVariation() {
    let startTime = Date.now();
    info("starting test: " + props.name);
    await testWithProperties(props, startTime);
    await resetPrefs();
  });
}

add_task(async function testRemoteSettingsEnable() {
  let startTime = Date.now();
  // Enable the rollout.
  await DoHTestUtils.loadRemoteSettingsConfig({
    providers: "example-1, example-2",
    rolloutEnabled: true,
    steeringEnabled: false,
    steeringProviders: "",
    autoDefaultEnabled: false,
    autoDefaultProviders: "",
    id: "global",
  });

  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
  let doc = gBrowser.selectedBrowser.contentDocument;

  info(Date.now() - startTime + ": testWithProperties: tab now open");
  let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");

  is(modeRadioGroup.value, "0", "expecting default mode");

  let status = doc.getElementById("dohStatus");
  await TestUtils.waitForCondition(
    () => document.l10n.getAttributes(status).args.status == "Active",
    "Waiting for remote settings to be processed"
  );
  is(
    document.l10n.getAttributes(status).args.status,
    "Active",
    "expecting status active"
  );

  let provider = doc.getElementById("dohResolver");
  is(
    provider.hidden,
    false,
    "Provider should not be hidden when status is active"
  );
  await TestUtils.waitForCondition(
    () =>
      document.l10n.getAttributes(provider).args.name ==
      DoHConfigController.currentConfig.providerList[0].UIName,
    "waiting for correct UI name"
  );
  is(
    document.l10n.getAttributes(provider).args.name,
    DoHConfigController.currentConfig.providerList[0].UIName,
    "expecting the right provider name"
  );

  let option = doc.getElementById("dohEnabledRadio");
  option.scrollIntoView();
  let win = doc.ownerGlobal;
  EventUtils.synthesizeMouseAtCenter(option, {}, win);

  await TestUtils.waitForCondition(
    () => Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics"),
    "Waiting for disable-heuristics"
  );
  is(provider.hidden, false);
  await TestUtils.waitForCondition(
    () =>
      document.l10n.getAttributes(provider).args.name ==
      DoHConfigController.currentConfig.providerList[0].UIName,
    "waiting for correct UI name"
  );
  is(
    document.l10n.getAttributes(provider).args.name,
    DoHConfigController.currentConfig.providerList[0].UIName,
    "expecting the right provider name"
  );
  is(
    Services.prefs.getIntPref("network.trr.mode"),
    Ci.nsIDNSService.MODE_TRRFIRST
  );

  option = doc.getElementById("dohOffRadio");
  option.scrollIntoView();
  win = doc.ownerGlobal;
  EventUtils.synthesizeMouseAtCenter(option, {}, win);
  await TestUtils.waitForCondition(
    () => status.innerHTML == "Status: Off",
    "Waiting for Status OFF"
  );
  is(
    Services.prefs.getIntPref("network.trr.mode"),
    Ci.nsIDNSService.MODE_TRROFF
  );
  is(provider.hidden, true, "Expecting provider to be hidden when DoH is off");

  gBrowser.removeCurrentTab();

  await DoHTestUtils.loadRemoteSettingsConfig({
    providers: "",
    rolloutEnabled: false,
    steeringEnabled: false,
    steeringProviders: "",
    autoDefaultEnabled: false,
    autoDefaultProviders: "",
    id: "global",
  });
});

add_task(async function testEnterprisePolicy() {
  async function withPolicy(policy, fn, preFn = () => {}) {
    await resetPrefs();
    PoliciesPrefTracker.start();
    await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy);
    await preFn();

    await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
    let doc = gBrowser.selectedBrowser.contentDocument;

    let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup");
    let resolverMenulist = doc.getElementById("dohEnabledResolverChoices");
    let uriTextbox = doc.getElementById("dohEnabledInputField");

    await fn({
      modeRadioGroup,
      resolverMenulist,
      doc,
      uriTextbox,
    });

    gBrowser.removeCurrentTab();
    // Set an empty policy before stopping the policy tracker
    await EnterprisePolicyTesting.setupPolicyEngineWithJson({});
    EnterprisePolicyTesting.resetRunOnceState();
    PoliciesPrefTracker.stop();
  }

  info("Check that a locked policy does not allow any changes in the UI");
  await withPolicy(
    {
      policies: {
        DNSOverHTTPS: {
          Enabled: true,
          ProviderURL: "https://examplelocked.com/provider",
          ExcludedDomains: ["examplelocked.com", "example.org"],
          Locked: true,
        },
      },
    },
    async res => {
      is(res.modeRadioGroup.disabled, true, "The mode menu should be locked.");
      is(res.modeRadioGroup.value, "2", "Should be enabled");
      is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
      is(
        res.uriTextbox.value,
        "https://examplelocked.com/provider",
        "Custom URI should be set"
      );
    }
  );

  info("Check that an unlocked policy has editable fields in the dialog");
  await withPolicy(
    {
      policies: {
        DNSOverHTTPS: {
          Enabled: true,
          ProviderURL: "https://example.com/provider",
          ExcludedDomains: ["example.com", "example.org"],
        },
      },
    },
    async res => {
      is(
        res.modeRadioGroup.disabled,
        false,
        "The mode menu should not be locked."
      );
      is(res.modeRadioGroup.value, "2", "Should be enabled");
      is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
      is(
        res.uriTextbox.value,
        "https://example.com/provider",
        "Expected custom resolver"
      );
    }
  );

  info("Check that a locked disabled policy disables the buttons");
  await withPolicy(
    {
      policies: {
        DNSOverHTTPS: {
          Enabled: false,
          ProviderURL: "https://example.com/provider",
          ExcludedDomains: ["example.com", "example.org"],
          Locked: true,
        },
      },
    },
    async res => {
      is(res.modeRadioGroup.disabled, true, "The mode menu should be locked.");
      is(res.modeRadioGroup.value, "5", "Should be disabled");
    }
  );

  info("Check that an unlocked disabled policy has editable fields");
  await withPolicy(
    {
      policies: {
        DNSOverHTTPS: {
          Enabled: false,
          ProviderURL: "https://example.com/provider",
          ExcludedDomains: ["example.com", "example.org"],
        },
      },
    },
    async res => {
      is(
        res.modeRadioGroup.disabled,
        false,
        "The mode menu should not be locked."
      );
      is(res.modeRadioGroup.value, "5", "Should be disabled");
    }
  );

  info("Check that the remote settings config doesn't override the policy");
  await withPolicy(
    {
      policies: {
        DNSOverHTTPS: {
          Enabled: true,
          ProviderURL: "https://example.com/provider",
          ExcludedDomains: ["example.com", "example.org"],
        },
      },
    },
    async res => {
      is(
        res.modeRadioGroup.disabled,
        false,
        "The mode menu should not be locked."
      );
      is(res.resolverMenulist.value, "custom", "Resolver list shows custom");
      is(
        res.uriTextbox.value,
        "https://example.com/provider",
        "Expected custom resolver"
      );
    },
    async function runAfterSettingPolicy() {
      await DoHTestUtils.loadRemoteSettingsConfig({
        providers: "example-1, example-2",
        rolloutEnabled: true,
        steeringEnabled: false,
        steeringProviders: "",
        autoDefaultEnabled: false,
        autoDefaultProviders: "",
        id: "global",
      });
    }
  );
});
