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

import assert from "assert";
import globalsUtils from "../lib/globals.mjs";
import path from "path";

var { getGlobalsForCode } = globalsUtils;

/* global describe, it, beforeEach */

describe("global variables", function () {
  it("should reflect top-level this property assignment", function () {
    const globals = getGlobalsForCode(`
this.foo = 10;
`);
    assert.deepEqual(globals, [{ name: "foo", writable: true }]);
  });

  it("should reflect this property assignment inside block", function () {
    const globals = getGlobalsForCode(`
{
  this.foo = 10;
}
`);
    assert.deepEqual(globals, [{ name: "foo", writable: true }]);
  });

  it("should ignore this property assignment inside function declaration", function () {
    const globals = getGlobalsForCode(`
function f() {
  this.foo = 10;
}
`);
    assert.deepEqual(globals, [{ name: "f", writable: true }]);
  });

  it("should ignore this property assignment inside function expression", function () {
    const globals = getGlobalsForCode(`
(function f() {
  this.foo = 10;
});
`);
    assert.deepEqual(globals, []);
  });

  it("should ignore this property assignment inside method", function () {
    const globals = getGlobalsForCode(`
({
  method() {
    this.foo = 10;
  }
});
`);
    assert.deepEqual(globals, []);
  });

  it("should ignore this property assignment inside accessor", function () {
    const globals = getGlobalsForCode(`
({
  get x() {
    this.foo = 10;
  },
  set x(v) {
    this.bar = 10;
  }
});
`);
    assert.deepEqual(globals, []);
  });

  it("should reflect this property assignment inside arrow function", function () {
    const globals = getGlobalsForCode(`
() => {
  this.foo = 10;
};
`);
    assert.deepEqual(globals, [{ name: "foo", writable: true }]);
  });

  it("should ignore this property assignment inside arrow function inside function expression", function () {
    const globals = getGlobalsForCode(`
(function f() {
  () => {
    this.foo = 10;
  };
});
`);
    assert.deepEqual(globals, []);
  });

  it("should ignore this property assignment inside class static", function () {
    const globals = getGlobalsForCode(`
class A {
  static {
    this.foo = 10;
    (() => {
      this.bar = 10;
    })();
  }
}
`);
    assert.deepEqual(globals, [{ name: "A", writable: true }]);
  });

  it("should ignore this property assignment inside class property", function () {
    const globals = getGlobalsForCode(`
class A {
  a = this.foo = 10;
  b = (() => {
    this.bar = 10;
  })();
}
`);
    assert.deepEqual(globals, [{ name: "A", writable: true }]);
  });

  it("should ignore this property assignment inside class static property", function () {
    const globals = getGlobalsForCode(`
class A {
  static a = this.foo = 10;
  static b = (() => {
    this.bar = 10;
  })();
}
`);
    assert.deepEqual(globals, [{ name: "A", writable: true }]);
  });

  it("should ignore this property assignment inside class private property", function () {
    const globals = getGlobalsForCode(`
class A {
  #a = this.foo = 10;
  #b = (() => {
    this.bar = 10;
  })();
}
`);
    assert.deepEqual(globals, [{ name: "A", writable: true }]);
  });

  it("should ignore this property assignment inside class static private property", function () {
    const globals = getGlobalsForCode(`
class A {
  static #a = this.foo = 10;
  static #b = (() => {
    this.bar = 10;
  })();
}
`);
    assert.deepEqual(globals, [{ name: "A", writable: true }]);
  });

  it("should reflect lazy getters", function () {
    const globals = getGlobalsForCode(`
ChromeUtils.defineESModuleGetters(this, {
  A: "B",
});
`);
    assert.deepEqual(globals, [{ name: "A", writable: true, explicit: true }]);
  });
});

function getFullTestFilePath(filename) {
  return import.meta.dirname + "/globals-data/" + filename;
}

describe("global import - file trees", () => {
  function callFn(filename) {
    return globalsUtils.ensureFileTree({
      filePath: getFullTestFilePath(filename),
    });
  }

  function stripPathsSet(set) {
    return new Set(set.values().map(v => path.basename(v)));
  }

  function stripPathsMap(map) {
    return new Map(
      map.entries().map(([k, v]) => [path.basename(k), stripPathsSet(v)])
    );
  }

  beforeEach(() => {
    globalsUtils.clearFileImportGraph();
  });

  it("should report errors where import-globals-from tries to import mjs files", () => {
    assert.throws(
      () => {
        callFn("import-globals-from-invalid.js");
      },
      {
        message:
          "import-globals-from does not support module files - use a direct import instead",
      }
    );
  });

  function assertImportGraph(testFilename, expectedMap, expectedSet) {
    let fileImportGraph = callFn(testFilename);

    assert.deepEqual(stripPathsMap(fileImportGraph), expectedMap);
    assert.deepEqual(
      stripPathsSet(
        fileImportGraph.getSubGraph(getFullTestFilePath(testFilename))
      ),
      expectedSet
    );
  }

  it("should build a tree when importing globals from another file", () => {
    assertImportGraph(
      "import-globals-from-1.js",
      new Map([
        ["import-globals-from-1-additional.js", new Set()],
        [
          "import-globals-from-1.js",
          new Set(["import-globals-from-1-additional.js"]),
        ],
      ]),
      new Set([
        "import-globals-from-1-additional.js",
        "import-globals-from-1.js",
      ])
    );
  });

  it("should handle loadSubScript", () => {
    assertImportGraph(
      "loadSubScript-1.js",
      new Map([
        ["loadSubScript-1.js", new Set(["loadSubScript-1-additional.js"])],
        ["loadSubScript-1-additional.js", new Set([])],
      ]),
      new Set(["loadSubScript-1-additional.js", "loadSubScript-1.js"])
    );
  });

  it("should handle importScripts in a worker", () => {
    assertImportGraph(
      "importScripts-1.js",
      new Map([
        ["importScripts-1.js", new Set(["importScripts-1-additional.js"])],
        ["importScripts-1-additional.js", new Set([])],
      ]),
      new Set(["importScripts-1-additional.js", "importScripts-1.js"])
    );
  });

  it("should build a tree handling a simple circular import", () => {
    assertImportGraph(
      "import-globals-from-circular-1.js",
      new Map([
        [
          "import-globals-from-circular-1.js",
          new Set(["import-globals-from-circular-2.js"]),
        ],
        [
          "import-globals-from-circular-2.js",
          new Set(["import-globals-from-circular-1.js"]),
        ],
      ]),
      new Set([
        "import-globals-from-circular-2.js",
        "import-globals-from-circular-1.js",
      ])
    );
  });

  it("should build a tree handling complex circular import", () => {
    assertImportGraph(
      "import-globals-from-circular-3.js",
      new Map([
        [
          "import-globals-from-circular-3.js",
          new Set(["import-globals-from-circular-4.js"]),
        ],
        [
          "import-globals-from-circular-4.js",
          new Set(["import-globals-from-circular-5.js"]),
        ],
        [
          "import-globals-from-circular-5.js",
          new Set(["import-globals-from-circular-3.js"]),
        ],
      ]),
      new Set([
        "import-globals-from-circular-3.js",
        "import-globals-from-circular-4.js",
        "import-globals-from-circular-5.js",
      ])
    );
  });
});

describe("global import - correct globals", () => {
  beforeEach(() => {
    globalsUtils.clearFileImportGraph();
  });

  function callFn(filename) {
    return globalsUtils.getGlobalsForFile({
      filePath: getFullTestFilePath(filename),
    });
  }

  it("should return the globals for the simple import case", () => {
    let globals = callFn("import-globals-from-1.js");

    assert.deepEqual(globals, [
      { name: "foo1", writable: true },
      { explicit: true, name: "foo", writable: true },
      { name: "bar1", writable: true },
      { explicit: true, name: "bar", writable: true },
    ]);
  });

  it("should return the globals for a simple circular import case", () => {
    let globals = callFn("import-globals-from-circular-1.js");

    assert.deepEqual(
      globals,
      [
        { name: "circ1", writable: true },
        { name: "circ2", writable: true },
      ],
      "should return the correct global when getting the globals for the first file"
    );

    globals = callFn("import-globals-from-circular-2.js");

    assert.deepEqual(
      globals,
      [
        { name: "circ2", writable: true },
        { name: "circ1", writable: true },
      ],
      "should return the correct global when getting the globals for the second file"
    );
  });

  it("should return the globals for a simple circular import case", () => {
    let globals = callFn("import-globals-from-circular-3.js");

    assert.deepEqual(
      globals,
      [
        { name: "circ3", writable: true },
        { name: "circ4", writable: true },
        { name: "circ5", writable: true },
      ],
      "should return the correct global when getting the globals for the first file"
    );

    globals = callFn("import-globals-from-circular-4.js");

    assert.deepEqual(
      globals,
      [
        { name: "circ4", writable: true },
        { name: "circ5", writable: true },
        { name: "circ3", writable: true },
      ],
      "should return the correct global when getting the globals for the second file"
    );

    globals = callFn("import-globals-from-circular-5.js");

    assert.deepEqual(
      globals,
      [
        { name: "circ5", writable: true },
        { name: "circ3", writable: true },
        { name: "circ4", writable: true },
      ],
      "should return the correct global when getting the globals for the third file"
    );
  });
});
