/* -*- Mode: js; js-indent-level: 2; -*- */
/*
 * Copyright 2011 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE or:
 * http://opensource.org/licenses/BSD-3-Clause
 */

const util = require("./util");
const SourceMapGenerator =
  require("../lib/source-map-generator").SourceMapGenerator;
const SourceMapConsumer =
  require("../lib/source-map-consumer").SourceMapConsumer;
const SourceNode = require("../lib/source-node").SourceNode;

function forEachNewline(fn) {
  return async function (assert) {
    await fn(assert, "\n");
    await fn(assert, "\r\n");
  };
}

exports["test .add()"] = function (assert) {
  const node = new SourceNode(null, null, null);

  // Adding a string works.
  node.add("function noop() {}");

  // Adding another source node works.
  node.add(new SourceNode(null, null, null));

  // Adding an array works.
  node.add([
    "function foo() {",
    new SourceNode(null, null, null, "return 10;"),
    "}",
  ]);

  // Adding other stuff doesn't.
  assert.throws(function () {
    node.add({});
  }, /TypeError: Expected a SourceNode, string, or an array of SourceNodes and strings/);
  assert.throws(function () {
    node.add(function () {});
  }, /TypeError: Expected a SourceNode, string, or an array of SourceNodes and strings/);
};

exports["test .prepend()"] = function (assert) {
  const node = new SourceNode(null, null, null);

  // Prepending a string works.
  node.prepend("function noop() {}");
  assert.equal(node.children[0], "function noop() {}");
  assert.equal(node.children.length, 1);

  // Prepending another source node works.
  node.prepend(new SourceNode(null, null, null));
  assert.equal(node.children[0], "");
  assert.equal(node.children[1], "function noop() {}");
  assert.equal(node.children.length, 2);

  // Prepending an array works.
  node.prepend([
    "function foo() {",
    new SourceNode(null, null, null, "return 10;"),
    "}",
  ]);
  assert.equal(node.children[0], "function foo() {");
  assert.equal(node.children[1], "return 10;");
  assert.equal(node.children[2], "}");
  assert.equal(node.children[3], "");
  assert.equal(node.children[4], "function noop() {}");
  assert.equal(node.children.length, 5);

  // Prepending other stuff doesn't.
  assert.throws(function () {
    node.prepend({});
  }, /TypeError: Expected a SourceNode, string, or an array of SourceNodes and strings/);
  assert.throws(function () {
    node.prepend(function () {});
  }, /TypeError: Expected a SourceNode, string, or an array of SourceNodes and strings/);
};

exports["test .toString()"] = function (assert) {
  assert.equal(
    new SourceNode(null, null, null, [
      "function foo() {",
      new SourceNode(null, null, null, "return 10;"),
      "}",
    ]).toString(),
    "function foo() {return 10;}"
  );
};

exports["test .join()"] = function (assert) {
  assert.equal(
    new SourceNode(null, null, null, ["a", "b", "c", "d"])
      .join(", ")
      .toString(),
    "a, b, c, d"
  );
};

exports["test .walk()"] = function (assert) {
  const node = new SourceNode(null, null, null, [
    "(function () {\n",
    "  ",
    new SourceNode(1, 0, "a.js", ["someCall()"]),
    ";\n",
    "  ",
    new SourceNode(2, 0, "b.js", ["if (foo) bar()"]),
    ";\n",
    "}());",
  ]);
  const expected = [
    { str: "(function () {\n", source: null, line: null, column: null },
    { str: "  ", source: null, line: null, column: null },
    { str: "someCall()", source: "a.js", line: 1, column: 0 },
    { str: ";\n", source: null, line: null, column: null },
    { str: "  ", source: null, line: null, column: null },
    { str: "if (foo) bar()", source: "b.js", line: 2, column: 0 },
    { str: ";\n", source: null, line: null, column: null },
    { str: "}());", source: null, line: null, column: null },
  ];
  let i = 0;
  node.walk(function (chunk, loc) {
    assert.equal(expected[i].str, chunk);
    assert.equal(expected[i].source, loc.source);
    assert.equal(expected[i].line, loc.line);
    assert.equal(expected[i].column, loc.column);
    i++;
  });
};

exports["test .replaceRight"] = function (assert) {
  let node;

  // Not nested
  node = new SourceNode(null, null, null, "hello world");
  node.replaceRight(/world/, "universe");
  assert.equal(node.toString(), "hello universe");

  // Nested
  node = new SourceNode(null, null, null, [
    new SourceNode(null, null, null, "hey sexy mama, "),
    new SourceNode(null, null, null, "want to kill all humans?"),
  ]);
  node.replaceRight(/kill all humans/, "watch Futurama");
  assert.equal(node.toString(), "hey sexy mama, want to watch Futurama?");
};

exports["test .toStringWithSourceMap()"] = forEachNewline(async function (
  assert,
  nl
) {
  const node = new SourceNode(null, null, null, [
    "(function () {" + nl,
    "  ",
    new SourceNode(1, 0, "a.js", "someCall", "originalCall"),
    new SourceNode(1, 8, "a.js", "()"),
    ";" + nl,
    "  ",
    new SourceNode(2, 0, "b.js", ["if (foo) bar()"]),
    ";" + nl,
    "}());",
  ]);
  const result = node.toStringWithSourceMap({
    file: "foo.js",
  });

  assert.equal(
    result.code,
    ["(function () {", "  someCall();", "  if (foo) bar();", "}());"].join(nl)
  );

  let map = result.map;
  const mapWithoutOptions = node.toStringWithSourceMap().map;

  assert.ok(
    map instanceof SourceMapGenerator,
    "map instanceof SourceMapGenerator"
  );
  assert.ok(
    mapWithoutOptions instanceof SourceMapGenerator,
    "mapWithoutOptions instanceof SourceMapGenerator"
  );
  assert.ok(!("file" in mapWithoutOptions));
  mapWithoutOptions._file = "foo.js";
  util.assertEqualMaps(assert, map.toJSON(), mapWithoutOptions.toJSON());

  map = await new SourceMapConsumer(map.toString());

  let actual;

  actual = map.originalPositionFor({
    line: 1,
    column: 4,
  });
  assert.equal(actual.source, null);
  assert.equal(actual.line, null);
  assert.equal(actual.column, null);

  actual = map.originalPositionFor({
    line: 2,
    column: 2,
  });
  assert.equal(actual.source, "a.js");
  assert.equal(actual.line, 1);
  assert.equal(actual.column, 0);
  assert.equal(actual.name, "originalCall");

  actual = map.originalPositionFor({
    line: 3,
    column: 2,
  });
  assert.equal(actual.source, "b.js");
  assert.equal(actual.line, 2);
  assert.equal(actual.column, 0);

  actual = map.originalPositionFor({
    line: 3,
    column: 16,
  });
  assert.equal(actual.source, null);
  assert.equal(actual.line, null);
  assert.equal(actual.column, null);

  actual = map.originalPositionFor({
    line: 4,
    column: 2,
  });
  assert.equal(actual.source, null);
  assert.equal(actual.line, null);
  assert.equal(actual.column, null);

  map.destroy();
});

exports["test .fromStringWithSourceMap()"] = forEachNewline(async function (
  assert,
  nl
) {
  const testCode = util.testGeneratedCode.replace(/\n/g, nl);
  let map = await new SourceMapConsumer(util.testMap);
  const node = SourceNode.fromStringWithSourceMap(testCode, map);
  map.destroy();

  const result = node.toStringWithSourceMap({
    file: "min.js",
  });
  map = result.map;
  const code = result.code;

  assert.equal(code, testCode);
  assert.ok(
    map instanceof SourceMapGenerator,
    "map instanceof SourceMapGenerator"
  );
  map = map.toJSON();
  assert.equal(map.version, util.testMap.version);
  assert.equal(map.file, util.testMap.file);
  assert.equal(map.mappings, util.testMap.mappings);
});

exports["test .fromStringWithSourceMap() empty map"] = forEachNewline(
  async function (assert, nl) {
    let map = await new SourceMapConsumer(util.emptyMap);
    const node = SourceNode.fromStringWithSourceMap(
      util.testGeneratedCode.replace(/\n/g, nl),
      map
    );
    map.destroy();

    const result = node.toStringWithSourceMap({
      file: "min.js",
    });
    map = result.map;
    const code = result.code;

    assert.equal(code, util.testGeneratedCode.replace(/\n/g, nl));
    assert.ok(
      map instanceof SourceMapGenerator,
      "map instanceof SourceMapGenerator"
    );
    map = map.toJSON();
    assert.equal(map.version, util.emptyMap.version);
    assert.equal(map.file, util.emptyMap.file);
    assert.equal(map.mappings.length, util.emptyMap.mappings.length);
    assert.equal(map.mappings, util.emptyMap.mappings);
  }
);

exports["test .fromStringWithSourceMap() complex version"] = forEachNewline(
  async function (assert, nl) {
    let input = new SourceNode(null, null, null, [
      "(function() {" + nl,
      "  var Test = {};" + nl,
      "  ",
      new SourceNode(1, 0, "a.js", "Test.A = { value: 1234 };" + nl),
      "  ",
      new SourceNode(2, 0, "a.js", "Test.A.x = 'xyz';"),
      nl,
      "}());" + nl,
      "/* Generated Source */",
    ]);
    input = input.toStringWithSourceMap({
      file: "foo.js",
    });

    let map = await new SourceMapConsumer(input.map.toString());
    const node = SourceNode.fromStringWithSourceMap(input.code, map);
    map.destroy();

    const result = node.toStringWithSourceMap({
      file: "foo.js",
    });
    map = result.map;
    const code = result.code;

    assert.equal(code, input.code);
    assert.ok(
      map instanceof SourceMapGenerator,
      "map instanceof SourceMapGenerator"
    );
    map = map.toJSON();
    const inputMap = input.map.toJSON();
    util.assertEqualMaps(assert, map, inputMap);
  }
);

exports["test .fromStringWithSourceMap() third argument"] = async function (
  assert
) {
  // Assume the following directory structure:
  //
  // http://foo.org/
  //   app/
  //     coffee/
  //       foo.coffee
  //       coffeeBundle.js # Made from {foo,bar,baz}.coffee
  //       maps/
  //         coffeeBundle.js.map
  //     js/
  //       foo.js
  //     public/
  //       app.js # Made from {foo,coffeeBundle}.js
  //       app.js.map

  let coffeeBundle = new SourceNode(1, 0, "foo.coffee", "foo(coffee);\n");
  coffeeBundle.setSourceContent("foo.coffee", "foo coffee");
  coffeeBundle = coffeeBundle.toStringWithSourceMap({
    file: "foo.js",
    sourceRoot: "..",
  });

  const foo = new SourceNode(1, 0, "foo.js", "foo(js);");

  const test = async function (relativePath, expectedSources) {
    const app = new SourceNode();

    const map = await new SourceMapConsumer(coffeeBundle.map.toString());
    app.add(
      SourceNode.fromStringWithSourceMap(coffeeBundle.code, map, relativePath)
    );
    map.destroy();

    app.add(foo);
    let i = 0;
    app.walk(function (chunk, loc) {
      assert.equal(loc.source, expectedSources[i]);
      i++;
    });
    app.walkSourceContents(function (sourceFile, sourceContent) {
      assert.equal(sourceFile, expectedSources[0]);
      assert.equal(sourceContent, "foo coffee");
    });
  };

  await test("../coffee/maps", ["../coffee/foo.coffee", "foo.js"]);

  // If the third parameter is omitted or set to the current working
  // directory we get incorrect source paths:

  await test(undefined, ["../foo.coffee", "foo.js"]);

  await test("", ["../foo.coffee", "foo.js"]);

  await test(".", ["../foo.coffee", "foo.js"]);

  await test("./", ["../foo.coffee", "foo.js"]);
};

exports["test .toStringWithSourceMap() merging duplicate mappings"] =
  forEachNewline(function (assert, nl) {
    let input = new SourceNode(null, null, null, [
      new SourceNode(1, 0, "a.js", "(function"),
      new SourceNode(1, 0, "a.js", "() {" + nl),
      "  ",
      new SourceNode(1, 0, "a.js", "var Test = "),
      new SourceNode(1, 0, "b.js", "{};" + nl),
      new SourceNode(2, 0, "b.js", "Test"),
      new SourceNode(2, 0, "b.js", ".A", "A"),
      new SourceNode(2, 20, "b.js", " = { value: ", "A"),
      "1234",
      new SourceNode(2, 40, "b.js", " };" + nl, "A"),
      "}());" + nl,
      "/* Generated Source */",
    ]);
    input = input.toStringWithSourceMap({
      file: "foo.js",
    });

    assert.equal(
      input.code,
      [
        "(function() {",
        "  var Test = {};",
        "Test.A = { value: 1234 };",
        "}());",
        "/* Generated Source */",
      ].join(nl)
    );

    let correctMap = new SourceMapGenerator({
      file: "foo.js",
    });
    correctMap.addMapping({
      generated: { line: 1, column: 0 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    // Here is no need for a empty mapping,
    // because mappings ends at eol
    correctMap.addMapping({
      generated: { line: 2, column: 2 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 2, column: 13 },
      source: "b.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 3, column: 0 },
      source: "b.js",
      original: { line: 2, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 3, column: 4 },
      source: "b.js",
      name: "A",
      original: { line: 2, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 3, column: 6 },
      source: "b.js",
      name: "A",
      original: { line: 2, column: 20 },
    });
    // This empty mapping is required,
    // because there is a hole in the middle of the line
    correctMap.addMapping({
      generated: { line: 3, column: 18 },
    });
    correctMap.addMapping({
      generated: { line: 3, column: 22 },
      source: "b.js",
      name: "A",
      original: { line: 2, column: 40 },
    });
    // Here is no need for a empty mapping,
    // because mappings ends at eol

    const inputMap = input.map.toJSON();
    correctMap = correctMap.toJSON();
    util.assertEqualMaps(assert, inputMap, correctMap);
  });

exports["test .toStringWithSourceMap() multi-line SourceNodes"] =
  forEachNewline(function (assert, nl) {
    let input = new SourceNode(null, null, null, [
      new SourceNode(
        1,
        0,
        "a.js",
        "(function() {" + nl + "var nextLine = 1;" + nl + "anotherLine();" + nl
      ),
      new SourceNode(2, 2, "b.js", "Test.call(this, 123);" + nl),
      new SourceNode(2, 2, "b.js", "this['stuff'] = 'v';" + nl),
      new SourceNode(2, 2, "b.js", "anotherLine();" + nl),
      "/*" + nl + "Generated" + nl + "Source" + nl + "*/" + nl,
      new SourceNode(3, 4, "c.js", "anotherLine();" + nl),
      "/*" + nl + "Generated" + nl + "Source" + nl + "*/",
    ]);
    input = input.toStringWithSourceMap({
      file: "foo.js",
    });

    assert.equal(
      input.code,
      [
        "(function() {",
        "var nextLine = 1;",
        "anotherLine();",
        "Test.call(this, 123);",
        "this['stuff'] = 'v';",
        "anotherLine();",
        "/*",
        "Generated",
        "Source",
        "*/",
        "anotherLine();",
        "/*",
        "Generated",
        "Source",
        "*/",
      ].join(nl)
    );

    let correctMap = new SourceMapGenerator({
      file: "foo.js",
    });
    correctMap.addMapping({
      generated: { line: 1, column: 0 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 2, column: 0 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 3, column: 0 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 4, column: 0 },
      source: "b.js",
      original: { line: 2, column: 2 },
    });
    correctMap.addMapping({
      generated: { line: 5, column: 0 },
      source: "b.js",
      original: { line: 2, column: 2 },
    });
    correctMap.addMapping({
      generated: { line: 6, column: 0 },
      source: "b.js",
      original: { line: 2, column: 2 },
    });
    correctMap.addMapping({
      generated: { line: 11, column: 0 },
      source: "c.js",
      original: { line: 3, column: 4 },
    });

    const inputMap = input.map.toJSON();
    correctMap = correctMap.toJSON();
    util.assertEqualMaps(assert, inputMap, correctMap);
  });

exports["test .toStringWithSourceMap() with empty string"] = function (assert) {
  const node = new SourceNode(1, 0, "empty.js", "");
  const result = node.toStringWithSourceMap();
  assert.equal(result.code, "");
};

exports["test .toStringWithSourceMap() with consecutive newlines"] =
  forEachNewline(function (assert, nl) {
    let input = new SourceNode(null, null, null, [
      "/***/" + nl + nl,
      new SourceNode(1, 0, "a.js", "'use strict';" + nl),
      new SourceNode(2, 0, "a.js", "a();"),
    ]);
    input = input.toStringWithSourceMap({
      file: "foo.js",
    });

    assert.equal(input.code, ["/***/", "", "'use strict';", "a();"].join(nl));

    let correctMap = new SourceMapGenerator({
      file: "foo.js",
    });
    correctMap.addMapping({
      generated: { line: 3, column: 0 },
      source: "a.js",
      original: { line: 1, column: 0 },
    });
    correctMap.addMapping({
      generated: { line: 4, column: 0 },
      source: "a.js",
      original: { line: 2, column: 0 },
    });

    const inputMap = input.map.toJSON();
    correctMap = correctMap.toJSON();
    util.assertEqualMaps(assert, inputMap, correctMap);
  });

exports["test setSourceContent with toStringWithSourceMap"] = async function (
  assert
) {
  const aNode = new SourceNode(1, 1, "a.js", "a");
  aNode.setSourceContent("a.js", "someContent");
  const node = new SourceNode(null, null, null, [
    "(function () {\n",
    "  ",
    aNode,
    "  ",
    new SourceNode(1, 1, "b.js", "b"),
    "}());",
  ]);
  node.setSourceContent("b.js", "otherContent");
  let map = node.toStringWithSourceMap({
    file: "foo.js",
  }).map;

  assert.ok(
    map instanceof SourceMapGenerator,
    "map instanceof SourceMapGenerator"
  );
  map = await new SourceMapConsumer(map.toString());

  assert.equal(map.sources.length, 2);
  assert.equal(map.sources[0], "a.js");
  assert.equal(map.sources[1], "b.js");
  assert.equal(map.sourcesContent.length, 2);
  assert.equal(map.sourcesContent[0], "someContent");
  assert.equal(map.sourcesContent[1], "otherContent");

  map.destroy();
};

exports["test walkSourceContents"] = function (assert) {
  const aNode = new SourceNode(1, 1, "a.js", "a");
  aNode.setSourceContent("a.js", "someContent");
  const node = new SourceNode(null, null, null, [
    "(function () {\n",
    "  ",
    aNode,
    "  ",
    new SourceNode(1, 1, "b.js", "b"),
    "}());",
  ]);
  node.setSourceContent("b.js", "otherContent");
  const results = [];
  node.walkSourceContents(function (sourceFile, sourceContent) {
    results.push([sourceFile, sourceContent]);
  });
  assert.equal(results.length, 2);
  assert.equal(results[0][0], "a.js");
  assert.equal(results[0][1], "someContent");
  assert.equal(results[1][0], "b.js");
  assert.equal(results[1][1], "otherContent");
};

exports["test from issue 258"] = async function (assert) {
  const node = new SourceNode();

  const reactCode =
    ";require(0);\n//# sourceMappingURL=/index.ios.map?platform=ios&dev=false&minify=true";

  const reactMap =
    // eslint-disable-next-line
    '{"version":3,"file":"/index.ios.bundle?platform=ios&dev=false&minify=true","sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"sources":["require-0.js"],"names":[],"mappings":"AAAA;","file":"require-0.js","sourcesContent":[";require(0);"]}}]}';

  const map = await new SourceMapConsumer(reactMap);
  node.add(SourceNode.fromStringWithSourceMap(reactCode, map));
  map.destroy();
};
