/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

const { Schemas } = ChromeUtils.importESModule(
  "resource://gre/modules/Schemas.sys.mjs"
);

let { SchemaAPIInterface } = ExtensionCommon;

const global = this;

let json = [
  {
    namespace: "revokableNs",

    permissions: ["revokableNs"],

    properties: {
      stringProp: {
        type: "string",
        writable: true,
      },

      revokableStringProp: {
        type: "string",
        permissions: ["revokableProp"],
        writable: true,
      },

      submoduleProp: {
        $ref: "submodule",
      },

      revokableSubmoduleProp: {
        $ref: "submodule",
        permissions: ["revokableProp"],
      },
    },

    types: [
      {
        id: "submodule",
        type: "object",
        functions: [
          {
            name: "sub_foo",
            type: "function",
            parameters: [],
            returns: { type: "integer" },
          },
        ],
      },
    ],

    functions: [
      {
        name: "func",
        type: "function",
        parameters: [],
      },

      {
        name: "revokableFunc",
        type: "function",
        parameters: [],
        permissions: ["revokableFunc"],
      },
    ],

    events: [
      {
        name: "onEvent",
        type: "function",
      },

      {
        name: "onRevokableEvent",
        type: "function",
        permissions: ["revokableEvent"],
      },
    ],
  },
];

let recorded = [];

function record(...args) {
  recorded.push(args);
}

function verify(expected) {
  for (let [i, rec] of expected.entries()) {
    Assert.deepEqual(recorded[i], rec, `Record ${i} matches`);
  }

  equal(recorded.length, expected.length, "Got expected number of records");

  recorded.length = 0;
}

registerCleanupFunction(() => {
  equal(recorded.length, 0, "No unchecked recorded events at shutdown");
});

let permissions = new Set();

class APIImplementation extends SchemaAPIInterface {
  constructor(namespace, name) {
    super();
    this.namespace = namespace;
    this.name = name;
  }

  record(method, args) {
    record(method, this.namespace, this.name, args);
  }

  revoke(...args) {
    this.record("revoke", args);
  }

  callFunction(...args) {
    this.record("callFunction", args);
    if (this.name === "sub_foo") {
      return 13;
    }
  }

  callFunctionNoReturn(...args) {
    this.record("callFunctionNoReturn", args);
  }

  getProperty(...args) {
    this.record("getProperty", args);
  }

  setProperty(...args) {
    this.record("setProperty", args);
  }

  addListener(...args) {
    this.record("addListener", args);
  }

  removeListener(...args) {
    this.record("removeListener", args);
  }

  hasListener(...args) {
    this.record("hasListener", args);
  }
}

let context = {
  manifestVersion: 2,
  cloneScope: global,

  permissionsChanged: null,

  setPermissionsChangedCallback(callback) {
    this.permissionsChanged = callback;
  },

  hasPermission(permission) {
    return permissions.has(permission);
  },

  isPermissionRevokable(permission) {
    return permission.startsWith("revokable");
  },

  getImplementation(namespace, name) {
    return new APIImplementation(namespace, name);
  },

  shouldInject() {
    return true;
  },
};

function ignoreError(fn) {
  try {
    fn();
  } catch (e) {
    // Meh.
  }
}

add_task(async function () {
  let url = "data:," + JSON.stringify(json);
  await Schemas.load(url);

  let root = {};
  Schemas.inject(root, context);
  equal(recorded.length, 0, "No recorded events");

  let listener = () => {};
  let captured = {};

  function checkRecorded() {
    let possible = [
      ["revokableNs", ["getProperty", "revokableNs", "stringProp", []]],
      [
        "revokableProp",
        ["getProperty", "revokableNs", "revokableStringProp", []],
      ],

      [
        "revokableNs",
        ["setProperty", "revokableNs", "stringProp", ["stringProp"]],
      ],
      [
        "revokableProp",
        [
          "setProperty",
          "revokableNs",
          "revokableStringProp",
          ["revokableStringProp"],
        ],
      ],

      ["revokableNs", ["callFunctionNoReturn", "revokableNs", "func", [[]]]],
      [
        "revokableFunc",
        ["callFunctionNoReturn", "revokableNs", "revokableFunc", [[]]],
      ],

      [
        "revokableNs",
        ["callFunction", "revokableNs.submoduleProp", "sub_foo", [[]]],
      ],
      [
        "revokableProp",
        ["callFunction", "revokableNs.revokableSubmoduleProp", "sub_foo", [[]]],
      ],

      [
        "revokableNs",
        ["addListener", "revokableNs", "onEvent", [listener, []]],
      ],
      ["revokableNs", ["removeListener", "revokableNs", "onEvent", [listener]]],
      ["revokableNs", ["hasListener", "revokableNs", "onEvent", [listener]]],

      [
        "revokableEvent",
        ["addListener", "revokableNs", "onRevokableEvent", [listener, []]],
      ],
      [
        "revokableEvent",
        ["removeListener", "revokableNs", "onRevokableEvent", [listener]],
      ],
      [
        "revokableEvent",
        ["hasListener", "revokableNs", "onRevokableEvent", [listener]],
      ],
    ];

    let expected = [];
    if (permissions.has("revokableNs")) {
      for (let [perm, recording] of possible) {
        if (!perm || permissions.has(perm)) {
          expected.push(recording);
        }
      }
    }

    verify(expected);
  }

  function check() {
    info(`Check normal access (permissions: [${Array.from(permissions)}])`);

    let ns = root.revokableNs;

    void ns.stringProp;
    void ns.revokableStringProp;

    ns.stringProp = "stringProp";
    ns.revokableStringProp = "revokableStringProp";

    ns.func();

    if (ns.revokableFunc) {
      ns.revokableFunc();
    }

    ns.submoduleProp.sub_foo();
    if (ns.revokableSubmoduleProp) {
      ns.revokableSubmoduleProp.sub_foo();
    }

    ns.onEvent.addListener(listener);
    ns.onEvent.removeListener(listener);
    ns.onEvent.hasListener(listener);

    if (ns.onRevokableEvent) {
      ns.onRevokableEvent.addListener(listener);
      ns.onRevokableEvent.removeListener(listener);
      ns.onRevokableEvent.hasListener(listener);
    }

    checkRecorded();
  }

  function capture() {
    info("Capture values");

    let ns = root.revokableNs;

    captured = { ns };
    captured.revokableStringProp = Object.getOwnPropertyDescriptor(
      ns,
      "revokableStringProp"
    );

    captured.revokableSubmoduleProp = ns.revokableSubmoduleProp;
    if (ns.revokableSubmoduleProp) {
      captured.sub_foo = ns.revokableSubmoduleProp.sub_foo;
    }

    captured.revokableFunc = ns.revokableFunc;

    captured.onRevokableEvent = ns.onRevokableEvent;
    if (ns.onRevokableEvent) {
      captured.addListener = ns.onRevokableEvent.addListener;
      captured.removeListener = ns.onRevokableEvent.removeListener;
      captured.hasListener = ns.onRevokableEvent.hasListener;
    }
  }

  function checkCaptured() {
    info(
      `Check captured value access (permissions: [${Array.from(permissions)}])`
    );

    let { ns } = captured;

    void ns.stringProp;
    ignoreError(() => captured.revokableStringProp.get());
    if (!permissions.has("revokableProp")) {
      void ns.revokableStringProp;
    }

    ns.stringProp = "stringProp";
    ignoreError(() => captured.revokableStringProp.set("revokableStringProp"));
    if (!permissions.has("revokableProp")) {
      ns.revokableStringProp = "revokableStringProp";
    }

    ignoreError(() => ns.func());
    ignoreError(() => captured.revokableFunc());
    if (!permissions.has("revokableFunc")) {
      ignoreError(() => ns.revokableFunc());
    }

    ignoreError(() => ns.submoduleProp.sub_foo());

    ignoreError(() => captured.sub_foo());
    if (!permissions.has("revokableProp")) {
      ignoreError(() => captured.revokableSubmoduleProp.sub_foo());
      ignoreError(() => ns.revokableSubmoduleProp.sub_foo());
    }

    ignoreError(() => ns.onEvent.addListener(listener));
    ignoreError(() => ns.onEvent.removeListener(listener));
    ignoreError(() => ns.onEvent.hasListener(listener));

    ignoreError(() => captured.addListener(listener));
    ignoreError(() => captured.removeListener(listener));
    ignoreError(() => captured.hasListener(listener));
    if (!permissions.has("revokableEvent")) {
      ignoreError(() => captured.onRevokableEvent.addListener(listener));
      ignoreError(() => captured.onRevokableEvent.removeListener(listener));
      ignoreError(() => captured.onRevokableEvent.hasListener(listener));

      ignoreError(() => ns.onRevokableEvent.addListener(listener));
      ignoreError(() => ns.onRevokableEvent.removeListener(listener));
      ignoreError(() => ns.onRevokableEvent.hasListener(listener));
    }

    checkRecorded();
  }

  permissions.add("revokableNs");
  permissions.add("revokableProp");
  permissions.add("revokableFunc");
  permissions.add("revokableEvent");

  check();
  capture();
  checkCaptured();

  permissions.delete("revokableProp");
  context.permissionsChanged();
  verify([
    ["revoke", "revokableNs", "revokableStringProp", []],
    ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []],
  ]);

  check();
  checkCaptured();

  permissions.delete("revokableFunc");
  context.permissionsChanged();
  verify([["revoke", "revokableNs", "revokableFunc", []]]);

  check();
  checkCaptured();

  permissions.delete("revokableEvent");
  context.permissionsChanged();

  verify([["revoke", "revokableNs", "onRevokableEvent", []]]);

  check();
  checkCaptured();

  permissions.delete("revokableNs");
  context.permissionsChanged();
  verify([
    ["revoke", "revokableNs", "stringProp", []],
    ["revoke", "revokableNs", "func", []],
    ["revoke", "revokableNs.submoduleProp", "sub_foo", []],
    ["revoke", "revokableNs", "onEvent", []],
  ]);

  checkCaptured();

  permissions.add("revokableNs");
  permissions.add("revokableProp");
  permissions.add("revokableFunc");
  permissions.add("revokableEvent");
  context.permissionsChanged();

  check();
  capture();
  checkCaptured();

  permissions.delete("revokableProp");
  permissions.delete("revokableFunc");
  permissions.delete("revokableEvent");
  context.permissionsChanged();
  verify([
    ["revoke", "revokableNs", "revokableStringProp", []],
    ["revoke", "revokableNs", "revokableFunc", []],
    ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []],
    ["revoke", "revokableNs", "onRevokableEvent", []],
  ]);

  check();
  checkCaptured();

  permissions.add("revokableProp");
  permissions.add("revokableFunc");
  permissions.add("revokableEvent");
  context.permissionsChanged();

  check();
  capture();
  checkCaptured();

  permissions.delete("revokableNs");
  context.permissionsChanged();
  verify([
    ["revoke", "revokableNs", "stringProp", []],
    ["revoke", "revokableNs", "revokableStringProp", []],
    ["revoke", "revokableNs", "func", []],
    ["revoke", "revokableNs", "revokableFunc", []],
    ["revoke", "revokableNs.submoduleProp", "sub_foo", []],
    ["revoke", "revokableNs.revokableSubmoduleProp", "sub_foo", []],
    ["revoke", "revokableNs", "onEvent", []],
    ["revoke", "revokableNs", "onRevokableEvent", []],
  ]);

  equal(root.revokableNs, undefined, "Namespace is not defined");
  checkCaptured();
});

add_task(async function test_neuter() {
  context.permissionsChanged = null;

  let root = {};
  Schemas.inject(root, context);
  equal(recorded.length, 0, "No recorded events");

  permissions.add("revokableNs");
  permissions.add("revokableProp");
  permissions.add("revokableFunc");
  permissions.add("revokableEvent");

  let ns = root.revokableNs;
  let { submoduleProp } = ns;

  let lazyGetter = Object.getOwnPropertyDescriptor(submoduleProp, "sub_foo");

  permissions.delete("revokableNs");
  context.permissionsChanged();
  verify([]);

  equal(root.revokableNs, undefined, "Should have no revokableNs");
  equal(ns.submoduleProp, undefined, "Should have no ns.submoduleProp");

  equal(submoduleProp.sub_foo, undefined, "No sub_foo");
  lazyGetter.get.call(submoduleProp);
  equal(submoduleProp.sub_foo, undefined, "No sub_foo");
});
