const { assert } = require("chai");
let Parser, JavaScript;

describe("Query", () => {
  let parser, tree, query;

  before(async () => ({ Parser, JavaScript } = await require("./helper")));

  beforeEach(() => {
    parser = new Parser().setLanguage(JavaScript);
  });

  afterEach(() => {
    parser.delete();
    if (tree) tree.delete();
    if (query) query.delete();
  });

  describe("construction", () => {
    it("throws an error on invalid patterns", () => {
      assert.throws(() => {
        JavaScript.query("(function_declaration wat)");
      }, "Bad syntax at offset 22: 'wat)'...");
      assert.throws(() => {
        JavaScript.query("(non_existent)");
      }, "Bad node name 'non_existent'");
      assert.throws(() => {
        JavaScript.query("(a)");
      }, "Bad node name 'a'");
      assert.throws(() => {
        JavaScript.query("(function_declaration non_existent:(identifier))");
      }, "Bad field name 'non_existent'");
      assert.throws(() => {
        JavaScript.query("(function_declaration name:(statement_block))");
      }, "Bad pattern structure at offset 22: 'name:(statement_block))'");
    });

    it("throws an error on invalid predicates", () => {
      assert.throws(() => {
        JavaScript.query("((identifier) @abc (#eq? @ab hi))");
      }, "Bad capture name @ab");
      assert.throws(() => {
        JavaScript.query("((identifier) @abc (#eq? @ab hi))");
      }, "Bad capture name @ab");
      assert.throws(() => {
        JavaScript.query("((identifier) @abc (#eq?))");
      }, "Wrong number of arguments to `#eq?` predicate. Expected 2, got 0");
      assert.throws(() => {
        JavaScript.query("((identifier) @a (eq? @a @a @a))");
      }, "Wrong number of arguments to `#eq?` predicate. Expected 2, got 3");
    });
  });

  describe(".matches", () => {
    it("returns all of the matches for the given query", () => {
      tree = parser.parse("function one() { two(); function three() {} }");
      query = JavaScript.query(`
        (function_declaration name: (identifier) @fn-def)
        (call_expression function: (identifier) @fn-ref)
      `);
      const matches = query.matches(tree.rootNode);
      assert.deepEqual(formatMatches(matches), [
        { pattern: 0, captures: [{ name: "fn-def", text: "one" }] },
        { pattern: 1, captures: [{ name: "fn-ref", text: "two" }] },
        { pattern: 0, captures: [{ name: "fn-def", text: "three" }] },
      ]);
    });

    it("can search in a specified ranges", () => {
      tree = parser.parse("[a, b,\nc, d,\ne, f,\ng, h]");
      query = JavaScript.query("(identifier) @element");
      const matches = query.matches(
        tree.rootNode,
        { row: 1, column: 1 },
        { row: 3, column: 1 }
      );
      assert.deepEqual(formatMatches(matches), [
        { pattern: 0, captures: [{ name: "element", text: "d" }] },
        { pattern: 0, captures: [{ name: "element", text: "e" }] },
        { pattern: 0, captures: [{ name: "element", text: "f" }] },
        { pattern: 0, captures: [{ name: "element", text: "g" }] },
      ]);
    });

    it("handles predicates that compare the text of capture to literal strings", () => {
      tree = parser.parse(`
        giraffe(1, 2, []);
        helment([false]);
        goat(false);
        gross(3, []);
        hiccup([]);
        gaff(5);
      `);

      // Find all calls to functions beginning with 'g', where one argument
      // is an array literal.
      query = JavaScript.query(`
        (call_expression
          function: (identifier) @name
          arguments: (arguments (array))
          (#match? @name "^g"))
      `);

      const matches = query.matches(tree.rootNode);
      assert.deepEqual(formatMatches(matches), [
        { pattern: 0, captures: [{name: "name", text: "giraffe" }] },
        { pattern: 0, captures: [{name: "name", text: "gross" }] },
      ]);
    });
  });

  describe(".captures", () => {
    it("returns all of the captures for the given query, in order", () => {
      tree = parser.parse(`
        a({
          bc: function de() {
            const fg = function hi() {}
          },
          jk: function lm() {
            const no = function pq() {}
          },
        });
      `);
      query = JavaScript.query(`
        (pair
          key: _ @method.def
          (function
            name: (identifier) @method.alias))

        (variable_declarator
          name: _ @function.def
          value: (function
            name: (identifier) @function.alias))

        ":" @delimiter
        "=" @operator
      `);

      const captures = query.captures(tree.rootNode);
      assert.deepEqual(formatCaptures(captures), [
        { name: "method.def", text: "bc" },
        { name: "delimiter", text: ":" },
        { name: "method.alias", text: "de" },
        { name: "function.def", text: "fg" },
        { name: "operator", text: "=" },
        { name: "function.alias", text: "hi" },
        { name: "method.def", text: "jk" },
        { name: "delimiter", text: ":" },
        { name: "method.alias", text: "lm" },
        { name: "function.def", text: "no" },
        { name: "operator", text: "=" },
        { name: "function.alias", text: "pq" },
      ]);
    });

    it("handles conditions that compare the text of capture to literal strings", () => {
      tree = parser.parse(`
        lambda
        panda
        load
        toad
        const ab = require('./ab');
        new Cd(EF);
      `);

      query = JavaScript.query(`
        ((identifier) @variable
         (#not-match? @variable "^(lambda|load)$"))

        ((identifier) @function.builtin
         (#eq? @function.builtin "require"))

        ((identifier) @constructor
         (#match? @constructor "^[A-Z]"))

        ((identifier) @constant
         (#match? @constant "^[A-Z]{2,}$"))
      `);

      const captures = query.captures(tree.rootNode);
      assert.deepEqual(formatCaptures(captures), [
        { name: "variable", text: "panda" },
        { name: "variable", text: "toad" },
        { name: "variable", text: "ab" },
        { name: "variable", text: "require" },
        { name: "function.builtin", text: "require" },
        { name: "variable", text: "Cd" },
        { name: "constructor", text: "Cd" },
        { name: "variable", text: "EF" },
        { name: "constructor", text: "EF" },
        { name: "constant", text: "EF" },
      ]);
    });

    it("handles conditions that compare the text of capture to each other", () => {
      tree = parser.parse(`
        ab = abc + 1;
        def = de + 1;
        ghi = ghi + 1;
      `);

      query = JavaScript.query(`
        (
          (assignment_expression
            left: (identifier) @id1
            right: (binary_expression
              left: (identifier) @id2))
          (#eq? @id1 @id2)
        )
      `);

      const captures = query.captures(tree.rootNode);
      assert.deepEqual(formatCaptures(captures), [
        { name: "id1", text: "ghi" },
        { name: "id2", text: "ghi" },
      ]);
    });

    it("handles patterns with properties", () => {
      tree = parser.parse(`a(b.c);`);
      query = JavaScript.query(`
        ((call_expression (identifier) @func)
         (#set! foo)
         (#set! bar baz))

        ((property_identifier) @prop
         (#is? foo)
         (#is-not? bar baz))
      `);

      const captures = query.captures(tree.rootNode);
      assert.deepEqual(formatCaptures(captures), [
        { name: "func", text: "a", setProperties: { foo: null, bar: "baz" } },
        {
          name: "prop",
          text: "c",
          assertedProperties: { foo: null },
          refutedProperties: { bar: "baz" },
        },
      ]);
      assert.ok(!query.didExceedMatchLimit());
    });

    it("detects queries with too many permutations to track", () => {
      tree = parser.parse(`
        [
          hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
          hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
          hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
          hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
          hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
        ];
      `);

      query = JavaScript.query(`
        (array (identifier) @pre (identifier) @post)
      `);

      const captures = query.captures(tree.rootNode, null, null, {matchLimit: 32});
      assert.ok(query.didExceedMatchLimit());
    });
  });

  describe(".predicatesForPattern(index)", () => {
    it("returns all of the predicates as objects", () => {
      query = JavaScript.query(`
        (
          (binary_expression
            left: (identifier) @a
            right: (identifier) @b)
          (#something? @a @b)
          (#match? @a "c")
          (#something-else? @a "A" @b "B")
        )

        ((identifier) @c
         (#hello! @c))

        "if" @d
      `);

      assert.deepEqual(query.predicatesForPattern(0), [
        {
          operator: "something?",
          operands: [
            { type: "capture", name: "a" },
            { type: "capture", name: "b" },
          ],
        },
        {
          operator: "something-else?",
          operands: [
            { type: "capture", name: "a" },
            { type: "string", value: "A" },
            { type: "capture", name: "b" },
            { type: "string", value: "B" },
          ],
        },
      ]);
      assert.deepEqual(query.predicatesForPattern(1), [
        {
          operator: "hello!",
          operands: [{ type: "capture", name: "c" }],
        },
      ]);
      assert.deepEqual(query.predicatesForPattern(2), []);
    });
  });
});

function formatMatches(matches) {
  return matches.map(({ pattern, captures }) => ({
    pattern,
    captures: formatCaptures(captures),
  }));
}

function formatCaptures(captures) {
  return captures.map((c) => {
    const node = c.node;
    delete c.node;
    c.text = node.text;
    return c;
  });
}
