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

import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs";

class MockWindowsRegKey {
  key = null;

  // --- Overridden nsISupports interface functions ---
  QueryInterface = ChromeUtils.generateQI(["nsIWindowsRegKey"]);

  #assertKey() {
    if (this.key) {
      return;
    }
    throw Components.Exception("invalid registry path", Cr.NS_ERROR_FAILURE);
  }

  #findOrMaybeCreateKey(root, path, mode, maybeCreateCallback) {
    let rootKey = MockRegistry.getRoot(root);
    let parts = path.split("\\");
    if (parts.some(part => !part.length)) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }

    let key = rootKey;
    for (let part of parts) {
      if (!key.subkeys.has(part)) {
        maybeCreateCallback(key.subkeys, part);
      }
      key = key.subkeys.get(part);
    }
    this.key = key;
  }

  // --- Overridden nsIWindowsRegKey interface functions ---
  open(root, path, mode) {
    // eslint-disable-next-line no-unused-vars
    this.#findOrMaybeCreateKey(root, path, mode, (subkeys, part) => {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    });
  }

  create(root, path, mode) {
    this.#findOrMaybeCreateKey(root, path, mode, (subkeys, part) =>
      subkeys.set(part, { subkeys: new Map(), values: new Map() })
    );
  }

  close() {
    this.key = null;
  }

  get valueCount() {
    this.#assertKey();
    return this.key.values.size;
  }

  hasValue(name) {
    this.#assertKey();
    return this.key.values.has(name);
  }

  #getValuePair(name, expectedType = null) {
    this.#assertKey();
    if (!this.key.values.has(name)) {
      throw Components.Exception("invalid value name", Cr.NS_ERROR_FAILURE);
    }
    let [value, type] = this.key.values.get(name);
    if (expectedType && type !== expectedType) {
      throw Components.Exception("unexpected value type", Cr.NS_ERROR_FAILURE);
    }
    return [value, type];
  }

  getValueType(name) {
    let [, type] = this.#getValuePair(name);
    return type;
  }

  getValueName(index) {
    if (!this.key || index >= this.key.values.size) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
    let names = Array.from(this.key.values.keys());
    return names[index];
  }

  readStringValue(name) {
    let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_STRING);
    return value;
  }

  readIntValue(name) {
    let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_INT);
    return value;
  }

  readInt64Value(name) {
    let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_INT64);
    return value;
  }

  readBinaryValue(name) {
    let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_BINARY);
    return value;
  }

  #writeValuePair(name, value, type) {
    this.#assertKey();
    this.key.values.set(name, [value, type]);
  }

  writeStringValue(name, value) {
    this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_STRING);
  }

  writeIntValue(name, value) {
    this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_INT);
  }

  writeInt64Value(name, value) {
    this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_INT64);
  }

  writeBinaryValue(name, value) {
    this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_BINARY);
  }

  removeValue(name) {
    this.#assertKey();
    this.key.values.delete(name);
  }

  get childCount() {
    this.#assertKey();
    return this.key.subkeys.size;
  }

  getChildName(index) {
    if (!this.key || index >= this.key.values.size) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
    let names = Array.from(this.key.subkeys.keys());
    return names[index];
  }

  hasChild(name) {
    this.#assertKey();
    return this.key.subkeys.has(name);
  }

  removeChild(name) {
    this.#assertKey();
    let child = this.key.subkeys.get(name);

    if (!child) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
    if (child.subkeys.size > 0) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }

    this.key.subkeys.delete(name);
  }

  #findOrMaybeCreateChild(name, mode, maybeCreateCallback) {
    this.#assertKey();
    if (name.split("\\").length > 1) {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
    if (!this.key.subkeys.has(name)) {
      maybeCreateCallback(this.key.subkeys, name);
    }
    // This won't wrap in the same way as `Cc["@mozilla.org/windows-registry-key;1"].createInstance(nsIWindowsRegKey);`.
    let subKey = new MockWindowsRegKey();
    subKey.key = this.key.subkeys.get(name);
    return subKey;
  }

  openChild(name, mode) {
    // eslint-disable-next-line no-unused-vars
    return this.#findOrMaybeCreateChild(name, mode, (subkeys, part) => {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    });
  }

  createChild(name, mode) {
    return this.#findOrMaybeCreateChild(name, mode, (subkeys, part) =>
      subkeys.set(part, { subkeys: new Map(), values: new Map() })
    );
  }
}

export class MockRegistry {
  // All instances of `MockRegistry` share a single data-store; this is
  // conceptually parallel to the Windows registry, which is a shared global
  // resource.  It would be possible to have separate data-stores for separate
  // instances with a little adjustment to `MockWindowsRegKey`.
  //
  // Top-level map is indexed by roots.  A "key" is an object that has
  // `subkeys` and `values`, both maps indexed by strings.  Subkey items are
  // again "key" objects.  Value items are `[value, type]` pairs.
  //
  // In pseudo-code:
  //
  // {Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
  //  {subkeys:
  //   {child: {subkeys: {}, values: {key: ["string_value", Ci.nsIWindowsRegKey.TYPE_STRING]}}},
  //  values: {}
  //  },
  //  ...
  // }
  static roots;

  constructor() {
    MockRegistry.roots = new Map([
      [
        Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
        { subkeys: new Map(), values: new Map() },
      ],
      [
        Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
        { subkeys: new Map(), values: new Map() },
      ],
      [
        Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
        { subkeys: new Map(), values: new Map() },
      ],
    ]);

    // See bug 1688838 - nsNotifyAddrListener::CheckAdaptersAddresses might
    // attempt to use the registry off the main thread, so we disable that
    // feature while the mock registry is active.
    this.oldSuffixListPref = Services.prefs.getBoolPref(
      "network.notify.dnsSuffixList"
    );
    Services.prefs.setBoolPref("network.notify.dnsSuffixList", false);

    this.oldCheckForProxiesPref = Services.prefs.getBoolPref(
      "network.notify.checkForProxies"
    );
    Services.prefs.setBoolPref("network.notify.checkForProxies", false);

    this.oldCheckForNRPTPref = Services.prefs.getBoolPref(
      "network.notify.checkForNRPT"
    );
    Services.prefs.setBoolPref("network.notify.checkForNRPT", false);

    this.cid = MockRegistrar.register(
      "@mozilla.org/windows-registry-key;1",
      () => new MockWindowsRegKey()
    );
  }

  shutdown() {
    MockRegistrar.unregister(this.cid);
    Services.prefs.setBoolPref(
      "network.notify.dnsSuffixList",
      this.oldSuffixListPref
    );
    Services.prefs.setBoolPref(
      "network.notify.checkForProxies",
      this.oldCheckForProxiesPref
    );
    Services.prefs.setBoolPref(
      "network.notify.checkForNRPT",
      this.oldCheckForNRPTPref
    );
    this.cid = null;
  }

  static getRoot(root) {
    if (!this.roots.has(root)) {
      throw new Error(`No such root ${root}`);
    }
    return this.roots.get(root);
  }

  setValue(root, path, name, value, type = Ci.nsIWindowsRegKey.TYPE_STRING) {
    let key = new MockWindowsRegKey();
    key.create(root, path, Ci.nsIWindowsRegKey.ACCESS_ALL);
    if (value == null) {
      try {
        key.removeValue(name);
      } catch (e) {
        if (
          !(e instanceof Ci.nsIException && e.result == Cr.NS_ERROR_FAILURE)
        ) {
          throw e;
        }
      }
    } else {
      switch (type) {
        case Ci.nsIWindowsRegKey.TYPE_STRING:
          key.writeStringValue(name, value);
          break;
        case Ci.nsIWindowsRegKey.TYPE_BINARY:
          key.writeBinaryValue(name, value);
          break;
        case Ci.nsIWindowsRegKey.TYPE_INT:
          key.writeIntValue(name, value);
          break;
        case Ci.nsIWindowsRegKey.TYPE_INT64:
          key.writeInt64Value(name, value);
          break;
      }
    }
  }

  /**
   * Dump given `key` (or, if not given, all roots), and all its value and its
   * subkeys recursively, using the given function to `printOneLine`.
   */
  static dump(key = null, indent = "", printOneLine = console.log) {
    let types = new Map([
      [1, "REG_SZ"],
      [3, "REG_BINARY"],
      [4, "REG_DWORD"],
      [11, "REG_QWORD"],
    ]);

    if (!key) {
      let roots = [
        "ROOT_KEY_LOCAL_MACHINE",
        "ROOT_KEY_CURRENT_USER",
        "ROOT_KEY_CLASSES_ROOT",
      ];
      for (let root of roots) {
        printOneLine(indent + root);
        this.dump(
          this.roots.get(Ci.nsIWindowsRegKey[root]),
          "  " + indent,
          printOneLine
        );
      }
    } else {
      for (let [k, v] of key.values.entries()) {
        let [value, type] = v;
        printOneLine(`${indent}${k}: ${value} (${types.get(type)})`);
      }
      for (let [k, child] of key.subkeys.entries()) {
        printOneLine(indent + k);
        this.dump(child, "  " + indent, printOneLine);
      }
    }
  }
}
