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

"use strict";

ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.import("resource://featuregates/FeatureGate.jsm", this);
ChromeUtils.import("resource://featuregates/FeatureGateImplementation.jsm", this);
ChromeUtils.import("resource://testing-common/httpd.js", this);

const kDefinitionDefaults = {
  id: "test-feature",
  title: "Test Feature",
  description: "A feature for testing",
  restartRequired: false,
  type: "boolean",
  preference: "test.feature",
  defaultValue: false,
  isPublic: false,
};

function definitionFactory(override = {}) {
  return Object.assign({}, kDefinitionDefaults, override);
}

class DefinitionServer {
  constructor(definitionOverrides = []) {
    this.server = new HttpServer();
    this.server.registerPathHandler("/definitions.json", this);
    this.definitions = {};

    for (const override of definitionOverrides) {
      this.addDefinition(override);
    }

    this.server.start();
    registerCleanupFunction(() => new Promise(resolve => this.server.stop(resolve)));
  }

  // for nsIHttpRequestHandler
  handle(request, response) {
    // response.setHeader("Content-Type", "application/json");
    response.write(JSON.stringify(this.definitions));
  }

  get definitionsUrl() {
    const {primaryScheme, primaryHost, primaryPort} = this.server.identity;
    return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`;
  }

  addDefinition(overrides = {}) {
    const definition = definitionFactory(overrides);
    // convert targeted values, used by fromId
    definition.isPublic = {default: definition.isPublic};
    definition.defaultValue = {default: definition.defaultValue};
    this.definitions[definition.id] = definition;
    return definition;
  }
}

// getValue should work
add_task(async function testGetValue() {
  const preference = "test.pref";
  equal(
    Services.prefs.getPrefType(preference),
    Services.prefs.PREF_INVALID,
    "Before creating the feature gate, the preference should not exist",
  );
  const feature = new FeatureGateImplementation(definitionFactory({ preference, defaultValue: false }));
  equal(
    Services.prefs.getPrefType(preference),
    Services.prefs.PREF_INVALID,
    "Instantiating a feature gate should not set its default value",
  );
  equal(await feature.getValue(), false, "getValue() should return the feature gate's default");

  Services.prefs.setBoolPref(preference, true);
  equal(await feature.getValue(), true, "getValue() should return the new value");

  Services.prefs.setBoolPref(preference, false);
  equal(await feature.getValue(), false, "getValue() should return the third value");

  // cleanup
  Services.prefs.getDefaultBranch("").deleteBranch(preference);
});

// event observers should work
add_task(async function testGetValue() {
  const preference = "test.pref";
  const feature = new FeatureGateImplementation(definitionFactory({ preference, defaultValue: false }));
  const observer = {
    onChange: sinon.stub(),
    onEnable: sinon.stub(),
    onDisable: sinon.stub(),
  };

  let rv = await feature.addObserver(observer);
  equal(rv, false, "addObserver returns the current value");

  Assert.deepEqual(observer.onChange.args, [], "onChange should not be called");
  Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called");
  Assert.deepEqual(observer.onDisable.args, [], "onDisable should not be called");

  Services.prefs.setBoolPref(preference, true);
  await Promise.resolve(); // Allow events to be called async
  Assert.deepEqual(observer.onChange.args, [[true]], "onChange should be called with the new value");
  Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called");
  Assert.deepEqual(observer.onDisable.args, [], "onDisable should not be called");

  Services.prefs.setBoolPref(preference, false);
  await Promise.resolve(); // Allow events to be called async
  Assert.deepEqual(observer.onChange.args, [[true], [false]], "onChange should be called again with the new value");
  Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should not be called a second time");
  Assert.deepEqual(observer.onDisable.args, [[]], "onDisable should be called for the first time");

  Services.prefs.setBoolPref(preference, false);
  await Promise.resolve(); // Allow events to be called async
  Assert.deepEqual(observer.onChange.args, [[true], [false]], "onChange should not be called if the value did not change");
  Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should not be called again if the value did not change");
  Assert.deepEqual(observer.onDisable.args, [[]], "onDisable should not be called if the value did not change");

  // cleanup
  feature.removeAllObservers();
  Services.prefs.getDefaultBranch("").deleteBranch(preference);
});
