/**
 * © Copyright IBM Corp. 2016 All Rights Reserved
 *   Project name: JSONata
 *   This project is licensed under the MIT License, see LICENSE
 *
 * The files in this directory are tests that aren't really portable
 * to other implementations for various reasons but they are included
 * in order to achieve 100% coverage for this implementation.
 */

"use strict";

var jsonata = require("../src/jsonata");
var chai = require("chai");
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
var expect = chai.expect;

var testdata1 = {
    "foo": {
        "bar": 42,
        "blah": [{"baz": {"fud": "hello"}}, {"baz": {"fud": "world"}}, {"bazz": "gotcha"}],
        "blah.baz": "here"
    }, "bar": 98
};

var testdata2 = {
    Account: {
        "Account Name": "Firefly",
        Order: [
            {
                OrderID: "order103",
                Product: [
                    {
                        "Product Name": "Bowler Hat",
                        ProductID: 858383,
                        SKU: "0406654608",
                        Description: {
                            Colour: "Purple",
                            Width: 300,
                            Height: 200,
                            Depth: 210,
                            Weight: 0.75
                        },
                        Price: 34.45,
                        Quantity: 2
                    },
                    {
                        "Product Name": "Trilby hat",
                        ProductID: 858236,
                        SKU: "0406634348",
                        Description: {
                            Colour: "Orange",
                            Width: 300,
                            Height: 200,
                            Depth: 210,
                            Weight: 0.6
                        },
                        Price: 21.67,
                        Quantity: 1
                    }
                ]
            },
            {
                OrderID: "order104",
                Product: [
                    {
                        "Product Name": "Bowler Hat",
                        ProductID: 858383,
                        SKU: "040657863",
                        Description: {
                            Colour: "Purple",
                            Width: 300,
                            Height: 200,
                            Depth: 210,
                            Weight: 0.75
                        },
                        Price: 34.45,
                        Quantity: 4
                    },
                    {
                        ProductID: 345664,
                        SKU: "0406654603",
                        "Product Name": "Cloak",
                        Description: {
                            Colour: "Black",
                            Width: 30,
                            Height: 20,
                            Depth: 210,
                            Weight: 2.0
                        },
                        Price: 107.99,
                        Quantity: 1
                    }
                ]
            }
        ]
    }
};

describe("Functions with side-effects", () => {
    describe("Evaluator - function: millis", function() {
        describe("$millis() returns milliseconds since the epoch", function() {
            it("should return result object", function() {
                var expr = jsonata("$millis()");
                // 27 Sep 2016, first commit to JSONata
                expect(expr.evaluate(testdata2)).to.eventually.be.above(
                    1474934400
                );
            });
        });

        describe("$millis() always returns same value within an expression", function() {
            it("should return result object", function() {
                var expr = jsonata(
                    '{"now": $millis(), "delay": $sum([1..10000]), "later": $millis()}.(now = later)'
                );
                expect(expr.evaluate(testdata2)).to.eventually.equal(true);
            });
        });

        describe("$millis() returns different timestamp for subsequent evaluate() calls", function() {
            it("should return result object", async function() {
                var expr = jsonata("($sum([1..10000]); $millis())");
                var result = await expr.evaluate(testdata2);
                var result2 = await expr.evaluate(testdata2);
                expect(result).to.not.equal(result2);
            });
        });
    });

    describe("$now() returns timestamp", function() {
        it("should return result object", function() {
            var expr = jsonata("$now()");
            var result = expr.evaluate(testdata2);
            // follows this pattern - "2017-05-09T10:10:16.918Z"
            expect(result).to.eventually.match(
                /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/
            );
        });
    });

    describe("$now() returns timestamp with defined format", function() {
        it("should return result object", function() {
            var expr = jsonata("$now('[h]:[M01][P] [z]')");
            var result = expr.evaluate(testdata2);
            // follows this pattern - "10:23am GMT+00:00"
            expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT\+00:00$/);
        });
    });

    describe("$now() returns timestamp with defined format and timezone", function() {
        it("should return result object", function() {
            var expr = jsonata("$now('[h]:[M01][P] [z]', '-0500')");
            var result = expr.evaluate(testdata2);
            // follows this pattern - "10:23am GMT-05:00"
            expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT-05:00$/);
        });
    });

    describe("$now() always returns same value within an expression", function() {
        it("should return result object", async function() {
            var expr = jsonata('{"now": $now(), "delay": $sum([1..10000]), "later": $now()}.(now = later)');
            var result = await expr.evaluate(testdata2);
            var expected = true;
            expect(result).to.deep.equal(expected);
        });
    });

    describe("$now() returns different timestamp for subsequent evaluate() calls", function() {
        it("should return result object", async function() {
            var expr = jsonata("($sum([1..100000]); $now())");
            var result = await expr.evaluate(testdata2);
            var result2 = await expr.evaluate(testdata2);
            expect(result).to.not.equal(result2);
        });
    });

    describe("$millis() returns milliseconds since the epoch", function() {
        it("should return result object", function() {
            var expr = jsonata("$millis()");
            // 27 Sep 2016, first commit to JSONata
            expect(expr.evaluate(testdata2)).to.eventually.be.above(1474934400);
        });
    });

    describe("Evaluator - functions: random", function() {
        describe('random number")', function() {
            it("should return result object", async function() {
                var expr = jsonata("$random()");
                var result = await expr.evaluate();
                var expected = result >= 0 && result < 1;
                expect(expected).to.equal(true);
            });
        });

        describe('consequetive random numbers should be different")', function() {
            it("should return result object", function() {
                var expr = jsonata("$random() = $random()");
                var expected = false;
                expect(expr.evaluate()).to.eventually.deep.equal(expected);
            });
        });
    });
});

describe("Tests that rely on JavaScript-style object traversal", () => {
    // A JSON object is an unordered list of key-value pairs.
    // When traversing an object, the entries may be returned
    // in a non-deterministic order (depending on the language).
    // The following tests assume a traversal order which works
    // in JavaScript but may not apply to other languages.
    // See https://github.com/jsonata-js/jsonata/issues/179.
    describe('foo.*[0]', function () {
        it('should return result object', async function () {
            var expr = jsonata("foo.*[0]");
            var result = await expr.evaluate(testdata1);
            var expected = 42;
            expect(result).to.deep.equal(expected);
        });
    });

    describe('**[2]', function () {
        it('should return result object', async function () {
            var expr = jsonata("**[2]");
            var result = await expr.evaluate(testdata2);
            var expected = "Firefly";
            expect(result).to.deep.equal(expected);
        });
    });
});

describe("Tests that use the $clone() function", () => {
    // $clone() allows jsonata-js to play nicely with Node-RED.
    // It's not part of the JSONata standard.
    // See https://github.com/jsonata-js/jsonata/issues/207.
    describe('clone undefined', function () {
        it('should return undefined', async function () {
            var expr = jsonata('$clone(foo)');
            var result = await expr.evaluate(testdata2);
            var expected = undefined;
            expect(result).to.deep.equal(expected);
        });
    });

    describe('clone empty object', function () {
        it('should return empty object', async function () {
            var expr = jsonata('$clone({})');
            var result = await expr.evaluate(testdata2);
            var expected = {};
            expect(result).to.deep.equal(expected);
        });
    });

    describe('clone object', function () {
        it('should return same object', async function () {
            var expr = jsonata('$clone({"a": 1})');
            var result = await expr.evaluate(testdata2);
            var expected = {"a": 1};
            expect(result).to.deep.equal(expected);
        });
    });

    describe("transform expression with overridden $clone function", function() {
        it("should return result object", async function() {
            var expr = jsonata('Account ~> |Order|{"Product":"blah"},nomatch|');
            var count = 0;
            expr.registerFunction("clone", function(arg) {
                count++;
                return JSON.parse(JSON.stringify(arg));
            });
            var result = await expr.evaluate(testdata2);
            var expected = {
                "Account Name": "Firefly",
                Order: [
                    {
                        OrderID: "order103",
                        Product: "blah"
                    },
                    {
                        OrderID: "order104",
                        Product: "blah"
                    }
                ]
            };
            expect(result).to.deep.equal(expected);
            expect(count).to.equal(1);
        });
    });

    describe('transform expression with overridden $clone value', function () {
        it('should throw error', async function () {
            var expr = jsonata('( $clone := 5; $ ~> |Account.Order.Product|{"blah":"foo"}| )');
            expect(
                expr.evaluate(testdata2)
            ).to.eventually.be.rejected.to.deep.contain({ code: "T2013" });
        });
    });
});

describe("Tests that bind Javascript functions", () => {
    // These involve binding of functions
    describe("Override implementation of $now()", function() {
        it("should return result object", async function() {
            var expr = jsonata("$now()");
            expr.registerFunction("now", function() {
                return "time for tea";
            });
            var result = await expr.evaluate(testdata2);
            expect(result).to.equal("time for tea");
        });
    });

    // Issue #261. Previously we would attempt to assign to the read-only `message` property,
    // causing an unrelated `TypeError` to be thrown instead
    describe("function throws a `DOMException` with a read-only `message` property", function() {
        /**
         * `DOMException` is not available in our testing environment. Additionally, we can't
         * just import the `domexception` module since it doesn't work on Node.js v4, which
         * we still support. So, here's a fake skeleton implementation which has the relevant
         * qualities we need to reproduce the bug, most importantly a read-only `message`
         * property
         * @param {string} message - Error message
         * @constructor
         */
        function DOMException (message) {
            Object.defineProperty(this, "message", {
                get() {
                    return message;
                },
                enumerable: true,
                configurable: true
            });
        }

        Object.setPrototypeOf(DOMException.prototype, Error.prototype);

        it("rethrows correctly", function() {
            var expr = jsonata("$throwDomEx()");
            expr.registerFunction("throwDomEx", function() {
                throw new DOMException('Here is my message');
            });
            expect(expr.evaluate({}))
                .to.eventually.be.rejectedWith(DOMException)
                .to.deep.contain({
                    message: "Here is my message",
                    position: 12,
                    token: "throwDomEx",
                });
        });
    });

    describe("map a user-defined Javascript function with signature", function() {
        it("should return result object", async function() {
            var expr = jsonata("$map([1,4,9,16], $squareroot)");
            expr.registerFunction(
                "squareroot",
                function(num) {
                    return Math.sqrt(num);
                },
                "<n:n>"
            );
            var result = await expr.evaluate(testdata2);
            var expected = [1, 2, 3, 4];
            expect(result).to.deep.equal(expected);
        });
    });
    describe("map a user-defined Javascript function with undefined signature", function() {
        it("should return result object", async function() {
            var expr = jsonata("$map([1,4,9,16], $squareroot)");
            expr.registerFunction("squareroot", function(num) {
                return Math.sqrt(num);
            });
            var result = await expr.evaluate(testdata2);
            var expected = [1, 2, 3, 4];
            expect(result).to.deep.equal(expected);
        });
    });

    describe("map a user-defined Javascript function", function() {
        it("should return result object", async function() {
            var expr = jsonata("$map([1,4,9,16], $squareroot)");
            expr.assign("squareroot", function(num) {
                return Math.sqrt(num);
            });
            var result = await expr.evaluate(testdata2);
            var expected = [1, 2, 3, 4];
            expect(result).to.deep.equal(expected);
        });
    });

    describe("$filter with a user-defined Javascript function", function() {
        it("should return result object", async function() {
            var expr = jsonata("$filter([1,4,9,16], $even)");
            expr.assign("even", function(num) {
                return num % 2 === 0;
            });
            var result = await expr.evaluate(testdata2);
            var expected = [4, 16];
            expect(result).to.deep.equal(expected);
        });
    });

    describe("$sift with a user-defined Javascript function", function() {
        it("should return result object", async function() {
            var expr = jsonata("$sift({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $even)");
            expr.assign("even", function(num) {
                return num % 2 === 0;
            });
            var result = await expr.evaluate(testdata2);
            var expected = {'four': 4, 'sixteen': 16};
            expect(result).to.deep.equal(expected);
        });
    });

    describe("$each with a user-defined Javascript function", function() {
        it("should return result object", async function() {
            var expr = jsonata("$each({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $squareroot)");
            expr.assign("squareroot", function(num) {
                return Math.sqrt(num);
            });
            var result = await expr.evaluate(testdata2);
            var expected = [1, 2, 3, 4];
            expect(result).to.deep.equal(expected);
        });
    });

    describe("Partially apply user-defined Javascript function", function() {
        it("should return result object", async function() {
            var expr = jsonata(
                "(" +
                    "  $firstn := $substr(?, 0, ?);" +
                    "  $first5 := $firstn(?, 5);" +
                    '  $first5("Hello World")' +
                    ")"
            );
            expr.assign("substr", function(str, start, len) {
                return str.substr(start, len);
            });
            var result = await expr.evaluate(testdata2);
            var expected = "Hello";
            expect(result).to.deep.equal(expected);
        });
    });

    describe("User defined matchers", function() {
        var repeatingLetters = function(char, repeat) {
            // custom matcher to match `repeat` contiguous occurrences of `char`
            var chars = char.repeat(repeat);
            var match = function(str, offset) {
                var pos = str.indexOf(chars, (offset || 0));
                if (pos === -1) {
                    return;
                } else {
                    return {
                        match: chars,
                        start: pos,
                        end: pos + chars.length,
                        groups: [],
                        next: function () {
                            return match(str, pos + chars.length);
                        }
                    };
                }
            };
            return match;
        };

        it("should match using a custom matcher", async function() {
            var expr = jsonata("$match('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
            expr.registerFunction("repeatingLetters", repeatingLetters);
            var result = await expr.evaluate();
            var expected = [
                {"match": "LL", "index": 0, "groups": []},
                {"match": "LL", "index": 10, "groups": []},
                {"match": "LL", "index": 18, "groups": []},
                {"match": "LL", "index": 37, "groups": []},
                {"match": "LL", "index": 39, "groups": []}
            ];
            expect(result).to.deep.equal(expected);
        });

        it("should split using a custom matcher", async function() {
            var expr = jsonata("$split('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
            expr.registerFunction("repeatingLetters", repeatingLetters);
            var result = await expr.evaluate();
            var expected = ["","ANFAIRPW","GWYNGY","GOGERYCHWYRNDROBW","","ANTYSILIOGOGOGOCH"];
            expect(result).to.deep.equal(expected);
        });

        it("should replace using a custom matcher", async function() {
            var expr = jsonata("$replace('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2), 'Ỻ')");
            expr.registerFunction("repeatingLetters", repeatingLetters);
            var result = await expr.evaluate();
            var expected = "ỺANFAIRPWỺGWYNGYỺGOGERYCHWYRNDROBWỺỺANTYSILIOGOGOGOCH";
            expect(result).to.deep.equal(expected);
        });

        it("should test inclusion using a custom matcher", async function() {
            var expr = jsonata("$contains('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 4))");
            expr.registerFunction("repeatingLetters", repeatingLetters);
            var result = await expr.evaluate();
            var expected = true;
            expect(result).to.deep.equal(expected);
        });

    });

    describe('User defined higher-order functions', () => {
        var myfunc = async (arr, fn) => 2 * (await fn(arr));

        var startsWith = function(str) {
            // returns a function that returns true if its argument starts with the string `str`
            return (arg) => {
                return arg.startsWith(str);
            };
        };

        it('should be able to invoke a built-in function passed as an argument', async () => {
            var expr = jsonata("$myfunc([1,2,3], $sum)");
            expr.registerFunction('myfunc', myfunc);
            var result = await expr.evaluate();
            var expected = 12;
            expect(result).to.deep.equal(expected);
        });

        it('should be able to invoke a lambda function passed as an argument', async () => {
            var expr = jsonata("$myfunc([1,2,3], λ($arr) { $arr[1] + $arr[2] })");
            expr.registerFunction('myfunc', myfunc);
            var result = await expr.evaluate();
            var expected = 10;
            expect(result).to.deep.equal(expected);
        });

        it('should be able to invoke a user-defined function passed as an argument', async () => {
            var expr = jsonata("$myfunc([1,2,3], $myfunc2)");
            expr.registerFunction('myfunc', myfunc);
            expr.registerFunction('myfunc2', (arr) => {
                return 2 * arr[1];
            });
            var result = await expr.evaluate();
            var expected = 8;
            expect(result).to.deep.equal(expected);
        });

        it('should be able to return a function from a user-defined function', async () => {
            var expr = jsonata(`
            (
              $startsWithHello := $startsWith("Hello");
              [$startsWithHello("Hello, Bob"), $startsWithHello("Goodbye, Bill")]
            )`);
            expr.registerFunction('startsWith', startsWith);
            var result = await expr.evaluate();
            var expected = [true, false];
            expect(result).to.deep.equal(expected);
        });
    });

    describe('User defined generator function', () => {
        var myAddFunc = function*(val) {
            yield val + 10;
        };

        var myArrayFunc = function *() {
            yield [1,2,3];
        };

        var myObjectFunc = function* () {
            yield {
                downloads: [
                    {
                        downloads: 1,
                        day: "2016-09-01",
                    },
                    {
                        downloads: 2,
                        day: "2016-09-02",
                    },
                    {
                        downloads: 3,
                        day: "2016-09-03",
                    },
                    {
                        downloads: 1453,
                        day: "2017-03-10",
                    },
                    {
                        downloads: 1194,
                        day: "2017-03-11",
                    },
                    {
                        downloads: 988,
                        day: "2017-03-12",
                    },
                ],
            };
        };

        it('should be able to invoke a generator function returning a simple value', async () => {
            var expr = jsonata("$myAddFunc(1)");
            expr.registerFunction('myAddFunc', myAddFunc);

            var result = await expr.evaluate();

            expect(result).to.equal(11);
        });

        it('should be able to invoke a generator function and map over its return array value', async () => {
            var expr = jsonata("$myArrayFunc().{\"foo\": \"bar\"}");
            expr.registerFunction('myArrayFunc', myArrayFunc);

            var result = await expr.evaluate();

            expect(result).to.deep.equal([
                {
                    "foo": "bar"
                },
                {
                    "foo": "bar"
                },
                {
                    "foo": "bar"
                }
            ]);
        });

        it('should be able to invoke a generator function and map over its return object value', async () => {
            var expr = jsonata("$myObjectFunc().downloads{ $substring(day, 0, 7): $sum(downloads) }");
            expr.registerFunction('myObjectFunc', myObjectFunc);

            var result = await expr.evaluate();

            expect(result).to.deep.equal({ '2016-09': 6, '2017-03': 3635 });
        });
    });

    describe('User defined higher-order generator functions', () => {
        var myfunc = function*(arr, fn) {
            const val = yield* fn(arr);
            return 2 * val;
        };

        // FIXME:
        it('a higher-order generator function will not work', async () => {
            var expr = jsonata("$myfunc([1,2,3], $sum)");
            expr.registerFunction('myfunc', myfunc);
            try {
                await expr.evaluate();
            } catch (e) {
                expect(e.message).to.equal("yield* (intermediate value)(intermediate value) is not iterable");
            }
        });
    });
});

describe("Tests that are specific to a Javascript runtime", () => {
    // Javascript specific
    describe('/ab/ ("ab")', function() {
        it("should return result object", async function() {
            var expr = jsonata('/ab/ ("ab")');
            var result = await expr.evaluate();
            var expected = { match: "ab", start: 0, end: 2, groups: [] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe("/ab/ ()", function() {
        it("should return result object", async function() {
            var expr = jsonata("/ab/ ()");
            var result = await expr.evaluate();
            var expected = undefined;
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/ab+/ ("ababbabbcc")', function() {
        it("should return result object", async function() {
            var expr = jsonata('/ab+/ ("ababbabbcc")');
            var result = await expr.evaluate();
            var expected = { match: "ab", start: 0, end: 2, groups: [] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/a(b+)/ ("ababbabbcc")', function() {
        it("should return result object", async function() {
            var expr = jsonata('/a(b+)/ ("ababbabbcc")');
            var result = await expr.evaluate();
            var expected = { match: "ab", start: 0, end: 2, groups: ["b"] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/a(b+)/ ("ababbabbcc").next()', function() {
        it("should return result object", async function() {
            var expr = jsonata('/a(b+)/ ("ababbabbcc").next()');
            var result = await expr.evaluate();
            var expected = { match: "abb", start: 2, end: 5, groups: ["bb"] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/a(b+)/ ("ababbabbcc").next().next()', function() {
        it("should return result object", async function() {
            var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next()');
            var result = await expr.evaluate();
            var expected = { match: "abb", start: 5, end: 8, groups: ["bb"] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/a(b+)/ ("ababbabbcc").next().next().next()', function() {
        it("should return result object", async function() {
            var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next().next()');
            var result = await expr.evaluate();
            var expected = undefined;
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe('/a(b+)/i ("Ababbabbcc")', function() {
        it("should return result object", async function() {
            var expr = jsonata('/a(b+)/i ("Ababbabbcc")');
            var result = await expr.evaluate();
            var expected = { match: "Ab", start: 0, end: 2, groups: ["b"] };
            expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
        });
    });

    describe("empty regex", function() {
        it("should throw error", function() {
            expect(function() {
                var expr = jsonata("//");
                expr.evaluate();
            })
                .to.throw()
                .to.deep.contain({ position: 1, code: "S0301" });
        });
    });

    describe("empty regex", function() {
        it("should throw error", function() {
            expect(function() {
                var expr = jsonata("/");
                expr.evaluate();
            })
                .to.throw()
                .to.deep.contain({ position: 1, code: "S0302" });
        });
    });

    describe("empty regex: Escaped termination", function() {
        it("should throw error", function() {
            expect(function() {
                var expr = jsonata("/\\/");
                expr.evaluate();
            })
                .to.throw()
                .to.deep.contain({ position: 3, code: "S0302" });
        });
    });

    describe("empty regex: Escaped termination", function() {
        it("should throw error", function() {
            expect(function() {
                var expr = jsonata("/\\\\\\/");
                expr.evaluate();
            })
                .to.throw()
                .to.deep.contain({ position: 5, code: "S0302" });
        });
    });

    describe("Functions - $match", function() {
        describe('$match("test escape \\\\", /\\\\/)', function() {
            it("should find \\", async function() {
                var expr = jsonata('$match("test escape \\\\", /\\\\/)');
                var result = await expr.evaluate();
                var expected = { match: "\\", index: 12, groups: []};
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("ababbabbcc",/ab/)', function() {
            it("should return result object", async function() {
                var expr = jsonata('$match("ababbabbcc",/ab/)');
                var result = await expr.evaluate();
                var expected = [
                    { match: "ab", index: 0, groups: [] },
                    {
                        match: "ab",
                        index: 2,
                        groups: []
                    },
                    { match: "ab", index: 5, groups: [] }
                ];
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("ababbabbcc",/a(b+)/)', function() {
            it("should return result object", async function() {
                var expr = jsonata('$match("ababbabbcc",/a(b+)/)');
                var result = await expr.evaluate();
                var expected = [
                    { match: "ab", index: 0, groups: ["b"] },
                    {
                        match: "abb",
                        index: 2,
                        groups: ["bb"]
                    },
                    { match: "abb", index: 5, groups: ["bb"] }
                ];
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("ababbabbcc",/a(b+)/, 1)', function() {
            it("should return result object", async function() {
                var expr = jsonata('$match("ababbabbcc",/a(b+)/, 1)');
                var result = await expr.evaluate();
                var expected = { match: "ab", index: 0, groups: ["b"] };
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("ababbabbcc",/a(b+)/, 0)', function() {
            it("should return result object", async function() {
                var expr = jsonata('$match("ababbabbcc",/a(b+)/, 0)');
                var result = await expr.evaluate();
                var expected = undefined;
                expect(result).to.deep.equal(expected);
            });
        });

        describe("$match(nothing,/a(xb+)/)", function() {
            it("should return result object", async function() {
                var expr = jsonata("$match(nothing,/a(xb+)/)");
                var result = await expr.evaluate();
                var expected = undefined;
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("ababbabbcc",/a(xb+)/)', function() {
            it("should return result object", async function() {
                var expr = jsonata('$match("ababbabbcc",/a(xb+)/)');
                var result = await expr.evaluate();
                var expected = undefined;
                expect(result).to.deep.equal(expected);
            });
        });

        describe('$match("a, b, c, d", /ab/, -3)', function() {
            it("should throw error", function() {
                var expr = jsonata('$match("a, b, c, d", /ab/, -3)');
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "D3040",
                    token: "match",
                    index: 3,
                    value: -3,
                });
            });
        });

        describe('$match("a, b, c, d", /ab/, null)', function() {
            it("should throw error", function() {
                var expr = jsonata('$match("a, b, c, d", /ab/, null)');
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 3,
                    value: null,
                });
            });
        });

        describe('$match("a, b, c, d", /ab/, "2")', function() {
            it("should throw error", function() {
                var expr = jsonata('$match("a, b, c, d", /ab/, "2")');
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 3,
                    value: "2",
                });
            });
        });

        describe('$match("a, b, c, d", "ab")', function() {
            it("should throw error", function() {
                var expr = jsonata('$match("a, b, c, d", "ab")');
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 2,
                    value: "ab",
                });
            });
        });

        describe('$match("a, b, c, d", true)', function() {
            it("should throw error", function() {
                var expr = jsonata('$match("a, b, c, d", true)');
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 2,
                    value: true,
                });
            });
        });

        describe("$match(12345, 3)", function() {
            it("should throw error", function() {
                var expr = jsonata("$match(12345, 3)");
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 1,
                    value: 12345,
                });
            });
        });

        describe("$match(12345)", function() {
            it("should throw error", function() {
                var expr = jsonata("$match(12345)");
                expect(
                    expr.evaluate()
                ).to.eventually.be.rejected.to.deep.contain({
                    position: 7,
                    code: "T0410",
                    token: "match",
                    index: 1,
                });
            });
        });
    });
});

describe("Test that yield platform specific results", () => {
    // Platform specific
    describe("$sqrt(10) * $sqrt(10)", function() {
        it("should return result object", async function() {
            var expr = jsonata("$sqrt(10) * $sqrt(10)");
            var result = await expr.evaluate();
            var expected = 10;
            expect(result).to.be.closeTo(expected, 1e-13);
        });
    });
});

describe("Tests that include infinite recursion", () => {
    describe("stack overflow - infinite recursive function - non-tail call", function() {
        it("should throw error", function() {
            var expr = jsonata("(" + "  $inf := function($n){$n+$inf($n-1)};" + "  $inf(5)" + ")");
            timeboxExpression(expr, 1000, 300);
            expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
                token: "inf",
                position: 32,
                code: "U1001",
            });
        });
    });

    describe("stack overflow - infinite recursive function - tail call", function() {
        this.timeout(5000);
        it("should throw error", function() {
            var expr = jsonata("( $inf := function(){$inf()}; $inf())");
            timeboxExpression(expr, 1000, 500);
            expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
                token: "inf",
                code: "U1001",
            });
        });
    });
});

/**
 * Protect the process/browser from a runnaway expression
 * i.e. Infinite loop (tail recursion), or excessive stack growth
 *
 * @param {Object} expr - expression to protect
 * @param {Number} timeout - max time in ms
 * @param {Number} maxDepth - max stack depth
 */
function timeboxExpression(expr, timeout, maxDepth) {
    var depth = 0;
    var time = Date.now();

    var checkRunnaway = function() {
        if (depth > maxDepth) {
            // stack too deep
            throw {
                message:
                    "Stack overflow error: Check for non-terminating recursive function.  Consider rewriting as tail-recursive.",
                stack: new Error().stack,
                code: "U1001"
            };
        }
        if (Date.now() - time > timeout) {
            // expression has run for too long
            throw {
                message: "Expression evaluation timeout: Check for infinite loop",
                stack: new Error().stack,
                code: "U1001"
            };
        }
    };

    // register callbacks
    expr.assign("__evaluate_entry", function() {
        depth++;
        checkRunnaway();
    });
    expr.assign("__evaluate_exit", function() {
        depth--;
        checkRunnaway();
    });
}