/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

import 'resource:///org/gnome/shell/ui/environment.js';

import * as JsParse from 'resource:///org/gnome/shell/misc/jsParse.js';

//
// Test javascript parsing
//
//
// TODO: We probably want to change all these to use
//       a table driven method, using for() inside
//       a test body hampers readibility and
//       debuggability when something goes wrong.
//
// NOTE: The inconsistent use of "" and '' quotes in this
//       file is largely to handle nesting without passing
//       escape markers to the functions under test. The
//       preferred style in the shell is single quotes
//       so we use that wherever possible.

describe('Matching quote search', () => {
    const FindMatchingQuoteParameters = [
        {
            name: 'only double quotes',
            input: "'double quotes'",
            output: 0,
        },
        {
            name: 'only single quotes',
            input: '\'single quotes\'',
            output: 0,
        },
        {
            name: 'some parts unquoted and other parts quoted',
            input: "some unquoted 'some quoted'",
            output: 14,
        },
        {
            name: 'mixed quotes',
            input: "'mixed \" quotes\"'",
            output: 0,
        },
        {
            name: 'escaped quotes',
            input: "'escaped \\' quote'",
            output: 0,
        },
    ];

    for (const {name, input, output} of FindMatchingQuoteParameters) {
        it(`finds a matching quote where there are ${name}`, () => {
            const match = JsParse.findMatchingQuote(input, input.length - 1);
            expect(match).toEqual(output);
        });
    }
});

describe('Matching slash search', () => {
    const FindMatchingSlashParameters = [
        {
            name: 'matching slashes',
            input: '/slash/',
            output: 0,
        },
        {
            name: 'matching slashes with extraneous characters in-between',
            input: '/slash " with $ funny ^\' stuff/',
            output: 0,
        },
        {
            name: 'mathcing slashes with some parts unslashed',
            input: 'some unslashed /some slashed/',
            output: 15,
        },
        {
            name: 'matching slashes with an escaped slash in the middle',
            input: '/escaped \\/ slash/',
            output: 0,
        },
    ];

    for (let {name, input, output} of FindMatchingSlashParameters) {
        it(`finds a matching slash where there are ${name}`, () => {
            let match = JsParse.findMatchingQuote(input, input.length - 1);
            expect(match).toEqual(output);
        });
    }
});

describe('Matching brace search', () => {
    const FindMatchingBraceParameters = [
        {
            name: 'square braces',
            input: '[square brace]',
            output: 0,
        },
        {
            name: 'round braces',
            input: '(round brace)',
            output: 0,
        },
        {
            name: 'braces with nesting',
            input: '([()][nesting!])',
            output: 0,
        },
        {
            name: 'braces with quoted braces in the middle',
            input: "[we have 'quoted [' braces]",
            output: 0,
        },
        {
            name: 'braces with regexed braces in the middle',
            input: '[we have /regex [/ braces]',
            output: 0,
        },
        {
            name: 'mismatched braces',
            input: '([[])[] mismatched braces ]',
            output: 1,
        },
    ];

    for (let {name, input, output} of FindMatchingBraceParameters) {
        it(`finds matching braces where there are ${name}`, () => {
            let match = JsParse.findMatchingBrace(input, input.length - 1);
            expect(match).toEqual(output);
        });
    }
});

describe('Beginning of expression search', () => {
    const ExpressionOffsetParameters = [
        {
            name: 'object property name',
            input: 'abc.123',
            output: 0,
        },
        {
            name: 'function call result property name',
            input: 'foo().bar',
            output: 0,
        },
        {
            name: 'centre of malformed function call expression',
            input: 'foo(bar',
            output: 4,
        },
        {
            name: 'complete nested expression',
            input: 'foo[abc.match(/"/)]',
            output: 0,
        },
    ];

    for (const {name, input, output} of ExpressionOffsetParameters) {
        it(`finds the beginning of a ${name}`, () => {
            const match = JsParse.getExpressionOffset(input, input.length - 1);
            expect(match).toEqual(output);
        });
    }
});

describe('Constant variable search', () => {
    const DeclaredConstantsParameters = [
        {
            name: 'two constants on one line with space between equals',
            input: 'const foo = X; const bar = Y',
            output: ['foo', 'bar'],
        },
        {
            name: 'two constants on one line with no space between equlas',
            input: 'const foo=X; const bar=Y;',
            output: ['foo', 'bar'],
        },
    ];

    for (const {name, input, output} of DeclaredConstantsParameters) {
        it(`finds ${name}`, () => {
            const match = JsParse.getDeclaredConstants(input);
            expect(match).toEqual(output);
        });
    }
});

describe('Expression safety determination', () => {
    const UnsafeExpressionParams = [
        {
            name: 'property access',
            input: 'foo.bar',
            output: false,
        },
        {
            name: 'property access by array',
            input: 'foo[\'bar\']',
            output: false,
        },
        {
            name: 'expression with syntax error',
            input: 'foo["a=b=c".match(/=/)',
            output: false,
        },
        {
            name: 'property access by array with nested const expression',
            input: 'foo[1==2]',
            output: false,
        },
        {
            name: 'bracketed assignment no whitespace',
            input: '(x=4)',
            output: true,
        },
        {
            name: 'bracked assignment with whitespace',
            input: '(x = 4)',
            output: true,
        },
        {
            name: 'bracketed implicit call',
            input: '(x;y)',
            output: true,
        },
    ];

    for (const {name, input, output} of UnsafeExpressionParams) {
        const isOrIsNot = output ? 'is' : 'is not';
        it(`finds that an expression which is a ${name} ${isOrIsNot} safe`, () => {
            let unsafe = JsParse.isUnsafeExpression(input);
            expect(unsafe).toEqual(output);
        });
    }
});

//
// Test safety of eval to get completions
//
describe('Expression evaluation', () => {
    const HARNESS_COMMAND_HEADER =
        'let imports = obj;' +
        'let global = obj;' +
        'let Main = obj;' +
        'let foo = obj;' +
        'let r = obj;';

    const ExpressionParameters = [
        "foo['a",
        "foo()['b'",
        "obj.foo()('a', 1, 2, 'b')().",
        'foo.[.',
        'foo]]]()))].',
        "123'ab\"",
        'Main.foo.bar = 3; bar.',
        '(Main.foo = 3).',
        'Main[Main.foo+=-1].',
    ];

    function evalIfSafe(text) {
        // Just as in JsParse.getCompletions, we will find the offset
        // of the expression, test whether it is unsafe, and then eval it.
        const offset = JsParse.getExpressionOffset(text, text.length - 1);
        if (offset < 0)
            return;

        const matches = text.slice(offset).match(/(.*)\.(.*)/);
        if (matches == null)
            return;

        const [, base] = matches;
        if (JsParse.isUnsafeExpression(base))
            return;

        eval(HARNESS_COMMAND_HEADER + base);
    }

    for (const expression of ExpressionParameters) {
        const globalPropsPre = Object.getOwnPropertyNames(globalThis).sort();

        it(`of ${expression} does not throw syntax errors with a known safe expression`, () => {
            expect(() => evalIfSafe(expression)).not.toThrow(SyntaxError);
        });

        const globalPropsPost = Object.getOwnPropertyNames(globalThis).sort();
        it(`of ${expression} does not modify the global scope`, () => {
            expect(globalPropsPre).toEqual(globalPropsPost);
        });
    }
});
