/**
 * @fileoverview Tests for NodeEventGenerator.
 * @author Toru Nagashima
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const assert = require("assert"),
    sinon = require("sinon"),
    espree = require("espree"),
    vk = require("eslint-visitor-keys"),
    Traverser = require("../../../lib/shared/traverser"),
    EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"),
    createEmitter = require("../../../lib/linter/safe-emitter"),
    NodeEventGenerator = require("../../../lib/linter/node-event-generator");


//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------

const ESPREE_CONFIG = {
    ecmaVersion: 6,
    comment: true,
    tokens: true,
    range: true,
    loc: true
};

const STANDARD_ESQUERY_OPTION = { visitorKeys: vk.KEYS, fallback: Traverser.getKeys };

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

describe("NodeEventGenerator", () => {
    EventGeneratorTester.testEventGeneratorInterface(
        new NodeEventGenerator(createEmitter(), STANDARD_ESQUERY_OPTION)
    );

    describe("entering a single AST node", () => {
        let emitter, generator;

        beforeEach(() => {
            emitter = Object.create(createEmitter(), { emit: { value: sinon.spy() } });

            ["Foo", "Bar", "Foo > Bar", "Foo:exit"].forEach(selector => emitter.on(selector, () => {}));
            generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);
        });

        it("should generate events for entering AST node.", () => {
            const dummyNode = { type: "Foo", value: 1 };

            generator.enterNode(dummyNode);

            assert(emitter.emit.calledOnce);
            assert(emitter.emit.calledWith("Foo", dummyNode));
        });

        it("should generate events for exitting AST node.", () => {
            const dummyNode = { type: "Foo", value: 1 };

            generator.leaveNode(dummyNode);

            assert(emitter.emit.calledOnce);
            assert(emitter.emit.calledWith("Foo:exit", dummyNode));
        });

        it("should generate events for AST queries", () => {
            const dummyNode = { type: "Bar", parent: { type: "Foo" } };

            generator.enterNode(dummyNode);

            assert(emitter.emit.calledTwice);
            assert(emitter.emit.calledWith("Foo > Bar", dummyNode));
        });
    });

    describe("traversing the entire AST", () => {

        /**
         * Gets a list of emitted types/selectors from the generator, in emission order
         * @param {ASTNode} ast The AST to traverse
         * @param {Array<string>|Set<string>} possibleQueries Selectors to detect
         * @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
         * array where the first element is a string, and the second element is the emitted AST node.
         */
        function getEmissions(ast, possibleQueries) {
            const emissions = [];
            const emitter = Object.create(createEmitter(), {
                emit: {
                    value: (selector, node) => emissions.push([selector, node])
                }
            });

            possibleQueries.forEach(query => emitter.on(query, () => {}));
            const generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);

            Traverser.traverse(ast, {
                enter(node, parent) {
                    node.parent = parent;
                    generator.enterNode(node);
                },
                leave(node) {
                    generator.leaveNode(node);
                }
            });

            return emissions;
        }

        /**
         * Creates a test case that asserts a particular sequence of generator emissions
         * @param {string} sourceText The source text that should be parsed and traversed
         * @param {string[]} possibleQueries A collection of selectors that rules are listening for
         * @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
         * generator is expected to produce, in order.
         * Each element of this list is an array where the first element is a selector (string), and the second is an AST node
         * This should only include emissions that appear in possibleQueries.
         * @returns {void}
         */
        function assertEmissions(sourceText, possibleQueries, expectedEmissions) {
            it(possibleQueries.join("; "), () => {
                const ast = espree.parse(sourceText, ESPREE_CONFIG);
                const emissions = getEmissions(ast, possibleQueries)
                    .filter(emission => possibleQueries.indexOf(emission[0]) !== -1);

                assert.deepStrictEqual(emissions, expectedEmissions(ast));
            });
        }

        assertEmissions(
            "foo + bar;",
            ["Program", "Program:exit", "ExpressionStatement", "ExpressionStatement:exit", "BinaryExpression", "BinaryExpression:exit", "Identifier", "Identifier:exit"],
            ast => [
                ["Program", ast], // entering program
                ["ExpressionStatement", ast.body[0]], // entering 'foo + bar;'
                ["BinaryExpression", ast.body[0].expression], // entering 'foo + bar'
                ["Identifier", ast.body[0].expression.left], // entering 'foo'
                ["Identifier:exit", ast.body[0].expression.left], // exiting 'foo'
                ["Identifier", ast.body[0].expression.right], // entering 'bar'
                ["Identifier:exit", ast.body[0].expression.right], // exiting 'bar'
                ["BinaryExpression:exit", ast.body[0].expression], // exiting 'foo + bar'
                ["ExpressionStatement:exit", ast.body[0]], // exiting 'foo + bar;'
                ["Program:exit", ast] // exiting program
            ]
        );

        assertEmissions(
            "foo + 5",
            [
                "BinaryExpression > Identifier",
                "BinaryExpression",
                "BinaryExpression Literal:exit",
                "BinaryExpression > Identifier:exit",
                "BinaryExpression:exit"
            ],
            ast => [
                ["BinaryExpression", ast.body[0].expression], // foo + 5
                ["BinaryExpression > Identifier", ast.body[0].expression.left], // foo
                ["BinaryExpression > Identifier:exit", ast.body[0].expression.left], // exiting foo
                ["BinaryExpression Literal:exit", ast.body[0].expression.right], // exiting 5
                ["BinaryExpression:exit", ast.body[0].expression] // exiting foo + 5
            ]
        );

        assertEmissions(
            "foo + 5",
            ["BinaryExpression > *[name='foo']"],
            ast => [["BinaryExpression > *[name='foo']", ast.body[0].expression.left]] // entering foo
        );

        assertEmissions(
            "foo",
            ["*"],
            ast => [
                ["*", ast], // Program
                ["*", ast.body[0]], // ExpressionStatement
                ["*", ast.body[0].expression] // Identifier
            ]
        );

        assertEmissions(
            "foo",
            ["*:not(ExpressionStatement)"],
            ast => [
                ["*:not(ExpressionStatement)", ast], // Program
                ["*:not(ExpressionStatement)", ast.body[0].expression] // Identifier
            ]
        );

        assertEmissions(
            "foo()",
            ["CallExpression[callee.name='foo']"],
            ast => [["CallExpression[callee.name='foo']", ast.body[0].expression]] // foo()
        );

        assertEmissions(
            "foo()",
            ["CallExpression[callee.name='bar']"],
            () => [] // (nothing emitted)
        );

        assertEmissions(
            "foo + bar + baz",
            [":not(*)"],
            () => [] // (nothing emitted)
        );

        assertEmissions(
            "foo + bar + baz",
            [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])"],
            ast => [
                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.left], // foo
                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.right], // bar
                [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.right] // baz
            ]
        );

        assertEmissions(
            "foo + 5 + 6",
            ["Identifier, Literal[value=5]"],
            ast => [
                ["Identifier, Literal[value=5]", ast.body[0].expression.left.left], // foo
                ["Identifier, Literal[value=5]", ast.body[0].expression.left.right] // 5
            ]
        );

        assertEmissions(
            "[foo, 5, foo]",
            ["Identifier + Literal"],
            ast => [["Identifier + Literal", ast.body[0].expression.elements[1]]] // 5
        );

        assertEmissions(
            "[foo, {}, 5]",
            ["Identifier + Literal", "Identifier ~ Literal"],
            ast => [["Identifier ~ Literal", ast.body[0].expression.elements[2]]] // 5
        );

        assertEmissions(
            "foo; bar + baz; qux()",
            [":expression", ":statement"],
            ast => [
                [":statement", ast.body[0]],
                [":expression", ast.body[0].expression],
                [":statement", ast.body[1]],
                [":expression", ast.body[1].expression],
                [":expression", ast.body[1].expression.left],
                [":expression", ast.body[1].expression.right],
                [":statement", ast.body[2]],
                [":expression", ast.body[2].expression],
                [":expression", ast.body[2].expression.callee]
            ]
        );

        assertEmissions(
            "function foo(){} var x; (function (p){}); () => {};",
            [":function", "ExpressionStatement > :function", "VariableDeclaration, :function[params.length=1]"],
            ast => [
                [":function", ast.body[0]], // function foo(){}
                ["VariableDeclaration, :function[params.length=1]", ast.body[1]], // var x;
                [":function", ast.body[2].expression], // function (p){}
                ["ExpressionStatement > :function", ast.body[2].expression], // function (p){}
                ["VariableDeclaration, :function[params.length=1]", ast.body[2].expression], // function (p){}
                [":function", ast.body[3].expression], // () => {}
                ["ExpressionStatement > :function", ast.body[3].expression] // () => {}
            ]
        );

        assertEmissions(
            "foo;",
            [
                "*",
                ":not(*)",
                "Identifier",
                "ExpressionStatement > *",
                "ExpressionStatement > Identifier",
                "ExpressionStatement > [name='foo']",
                "Identifier, ReturnStatement",
                "FooStatement",
                "[name = 'foo']",
                "[name='foo']",
                "[name ='foo']",
                "Identifier[name='foo']",
                "[name='foo'][name.length=3]",
                ":not(Program, ExpressionStatement)",
                ":not(Program, Identifier) > [name.length=3]"
            ],
            ast => [
                ["*", ast], // Program
                ["*", ast.body[0]], // ExpressionStatement

                // selectors for the 'foo' identifier, in order of increasing specificity
                ["*", ast.body[0].expression], // 0 identifiers, 0 pseudoclasses
                ["ExpressionStatement > *", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
                ["Identifier", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
                [":not(Program, ExpressionStatement)", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
                ["ExpressionStatement > Identifier", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
                ["Identifier, ReturnStatement", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
                ["[name = 'foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
                ["[name ='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
                ["[name='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
                ["ExpressionStatement > [name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
                ["Identifier[name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
                [":not(Program, Identifier) > [name.length=3]", ast.body[0].expression], // 1 attribute, 2 identifiers
                ["[name='foo'][name.length=3]", ast.body[0].expression] // 2 attributes, 0 identifiers
            ]
        );

        assertEmissions(
            "foo(); bar; baz;",
            ["CallExpression, [name='bar']"],
            ast => [
                ["CallExpression, [name='bar']", ast.body[0].expression],
                ["CallExpression, [name='bar']", ast.body[1].expression]
            ]
        );

        assertEmissions(
            "foo; bar;",
            ["[name.length=3]:exit"],
            ast => [
                ["[name.length=3]:exit", ast.body[0].expression],
                ["[name.length=3]:exit", ast.body[1].expression]
            ]
        );

        // https://github.com/eslint/eslint/issues/14799
        assertEmissions(
            "const {a = 1} = b;",
            ["Property > .key"],
            ast => [
                ["Property > .key", ast.body[0].declarations[0].id.properties[0].key]
            ]
        );
    });

    describe("traversing the entire non-standard AST", () => {

        /**
         * Gets a list of emitted types/selectors from the generator, in emission order
         * @param {ASTNode} ast The AST to traverse
         * @param {Record<string, string[]>} visitorKeys The custom visitor keys.
         * @param {Array<string>|Set<string>} possibleQueries Selectors to detect
         * @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
         * array where the first element is a string, and the second element is the emitted AST node.
         */
        function getEmissions(ast, visitorKeys, possibleQueries) {
            const emissions = [];
            const emitter = Object.create(createEmitter(), {
                emit: {
                    value: (selector, node) => emissions.push([selector, node])
                }
            });

            possibleQueries.forEach(query => emitter.on(query, () => {}));
            const generator = new NodeEventGenerator(emitter, { visitorKeys, fallback: Traverser.getKeys });

            Traverser.traverse(ast, {
                visitorKeys,
                enter(node, parent) {
                    node.parent = parent;
                    generator.enterNode(node);
                },
                leave(node) {
                    generator.leaveNode(node);
                }
            });

            return emissions;
        }

        /**
         * Creates a test case that asserts a particular sequence of generator emissions
         * @param {ASTNode} ast The AST to traverse
         * @param {Record<string, string[]>} visitorKeys The custom visitor keys.
         * @param {string[]} possibleQueries A collection of selectors that rules are listening for
         * @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
         * generator is expected to produce, in order.
         * Each element of this list is an array where the first element is a selector (string), and the second is an AST node
         * This should only include emissions that appear in possibleQueries.
         * @returns {void}
         */
        function assertEmissions(ast, visitorKeys, possibleQueries, expectedEmissions) {
            it(possibleQueries.join("; "), () => {
                const emissions = getEmissions(ast, visitorKeys, possibleQueries)
                    .filter(emission => possibleQueries.indexOf(emission[0]) !== -1);

                assert.deepStrictEqual(emissions, expectedEmissions(ast));
            });
        }

        assertEmissions(
            espree.parse("const foo = [<div/>, <div/>]", { ...ESPREE_CONFIG, ecmaFeatures: { jsx: true } }),
            vk.KEYS,
            ["* ~ *"],
            ast => [
                ["* ~ *", ast.body[0].declarations[0].init.elements[1]] // entering second JSXElement
            ]
        );

        assertEmissions(
            {

                // Parse `class A implements B {}` with typescript-eslint.
                type: "Program",
                errors: [],
                comments: [],
                sourceType: "module",
                body: [
                    {
                        type: "ClassDeclaration",
                        id: {
                            type: "Identifier",
                            name: "A"
                        },
                        superClass: null,
                        implements: [
                            {
                                type: "ClassImplements",
                                id: {
                                    type: "Identifier",
                                    name: "B"
                                },
                                typeParameters: null
                            }
                        ],
                        body: {
                            type: "ClassBody",
                            body: []
                        }
                    }
                ]
            },
            vk.unionWith({

                // see https://github.com/typescript-eslint/typescript-eslint/blob/e4d737b47574ff2c53cabab22853035dfe48c1ed/packages/visitor-keys/src/visitor-keys.ts#L27
                ClassDeclaration: [
                    "decorators",
                    "id",
                    "typeParameters",
                    "superClass",
                    "superTypeParameters",
                    "implements",
                    "body"
                ]
            }),
            [":first-child"],
            ast => [
                [":first-child", ast.body[0]], // entering first ClassDeclaration
                [":first-child", ast.body[0].implements[0]] // entering first ClassImplements
            ]
        );
    });

    describe("parsing an invalid selector", () => {
        it("throws a useful error", () => {
            const emitter = createEmitter();

            emitter.on("Foo >", () => {});
            assert.throws(
                () => new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION),
                /Syntax error in selector "Foo >" at position 5: Expected " ", "!", .*/u
            );
        });
    });
});
