    /*

    Siesta 5.3.0
    Copyright(c) 2009-2019 Bryntum AB
    https://bryntum.com/contact
    https://bryntum.com/products/siesta/license

    */
    /**
    @class Siesta.Test.BDD

    A mixin providing a BDD style layer for most of the assertion methods.
    It is consumed by {@link Siesta.Test}, so all of its methods are available in all tests.

    */
    Role('Siesta.Test.BDD', {

        requires    : [
            'getSubTest', 'chain'
        ],

        has         : {
            specType                : null, // `describe` or `it`

            beforeEachHooks         : Joose.I.Array,
            afterEachHooks          : Joose.I.Array,

            sequentialSubTests      : Joose.I.Array,

            // flag, whether the "run" function of the test (containing actual test code) have been already run
            codeProcessed           : false,

            launchTimeout           : null,

            // Siesta.Test.BDD.Expectation should already present on the page
            expectationClass        : Siesta.Test.BDD.Expectation,

            failOnExclusiveSpecsWhenAutomated   : false,

            spies                   : Joose.I.Array
        },

        methods     : {

            checkSpecFunction : function (func, type, name) {
                if (!func)          throw new Error(Siesta.Resource('Siesta.Test.BDD', 'codeBodyMissing') + " " + (type == 'describe' ? 'suite' : 'spec') + ' [' + name + ']')
                if (!func.length)   throw new Error(Siesta.Resource('Siesta.Test.BDD', 'codeBodyOf') + " " + (type == 'describe' ? 'suite' : 'spec') + ' [' + name + '] ' + Siesta.Resource('Siesta.Test.BDD', 'missingFirstArg'))
            },

            /**
             * This is an "exclusive" version of the regular {@link #describe} suite. When such suites presents in some test file,
             * the other regular suites at the same level will not be executed, only "exclusive" ones.
             *
             * @param {String} name The name or description of the suite
             * @param {Function} code The code function for this suite. It will receive a test instance as the first argument which should be used for all assertion methods.
             * @param {Number} [timeout] A maximum duration for this suite. If not provided {@link Siesta.Project#subTestTimeout} value is used.
             */
            ddescribe : function (name, code, timeout) {
                this.describe(name, code, timeout, true)
            },

            /**
             * This is a no-op method, allowing you to quickly ignore some suites.
             */
            xdescribe : function () {
            },

            /**
             * This method starts a sub test with *suite* (in BDD terms). Such suite consists from one or more *specs* (see method {@link #it}} or other suites.
             * The number of nesting levels is not limited. All suites of the same nesting level are executed sequentially.
             *
             * For example:
             *
        t.describe('A product', function (t) {

            t.it('should have feature X', function (t) {
                ...
            })

            t.describe('feature X', function (t) {
                t.it('should be cool', function (t) {
                    ...
                })
            })
        })
             *
             * See also {@link #beforeEach}, {@link #afterEach}, {@link #xdescribe}, {@link #ddescribe}
             *
             * @param {String} name The name or description of the suite
             * @param {Function} code The code function for this suite. It will receive a test instance as the first argument which should be used for all assertion methods.
             * @param {Number} [timeout] A maximum duration for this suite. If not provided {@link Siesta.Project#subTestTimeout} value is used.
             */
            describe : function (name, code, timeout, isExclusive) {
                this.checkSpecFunction(code, 'describe', name)

                var subTest     = this.getSubTest({
                    name            : name,
                    run             : code,

                    isExclusive     : isExclusive,

                    specType        : 'describe',
                    timeout         : timeout
                })

                if (this.codeProcessed) this.scheduleSpecsLaunch()

                this.sequentialSubTests.push(subTest)
            },

            /**
             * This is an "exclusive" version of the regular {@link #it} spec. When such specs presents in some suite,
             * the other regular specs at the same level will not be executed, only "exclusive" ones. Note, that even "regular" suites (`t.describe`) sections
             * will be ignored, if they are on the same level with the exclusive `iit` section.
             *
             * @param {String} name The name or description of the spec
             * @param {Function} code The code function for this spec. It will receive a test instance as the first argument which should be used for all assertion methods.
             * @param {Number} [timeout] A maximum duration for this spec. If not provided {@link Siesta.Project#subTestTimeout} value is used.
             */
            iit : function (name, code, timeout) {
                if (this.project.isAutomated) {
                    if (this.failOnExclusiveSpecsWhenAutomated) this.fail(Siesta.Resource('Siesta.Test.BDD', 'iitFound'));
                }
                this.it(name, code, timeout, true)
            },

            /**
             * This is a no-op method, allowing you to quickly ignore some specs.
             */
            xit : function () {
            },

            /**
             * This method starts a sub test with *spec* (in BDD terms). Such spec consists from one or more assertions (or *expectations*, *matchers*, etc) or other nested specs
             * and/or suites. See the {@link #expect} method. The number of nesting levels is not limited. All specs of the same nesting level are executed sequentially.
             *
             * For example:
             *
        t.describe('A product', function (t) {

            t.it('should have feature X', function (t) {
                ...
            })

            t.it('should have feature Y', function (t) {
                ...
            })
        })
             *
             * See also {@link #beforeEach}, {@link #afterEach}, {@link #xit}, {@link #iit}
             *
             * @param {String} name The name or description of the spec
             * @param {Function} code The code function for this spec. It will receive a test instance as the first argument which should be used for all assertion methods.
             * @param {Number} [timeout] A maximum duration for this spec. If not provided {@link Siesta.Project#subTestTimeout} value is used.
             */
            it : function (name, code, timeout, isExclusive, isTodo) {
                this.checkSpecFunction(code, 'it', name)

                var subTest     = this.getSubTest({
                    name            : name,
                    run             : code,

                    isExclusive     : isExclusive,
                    isTodo          : Boolean(isTodo) || this.isTodo,

                    specType        : 'it',
                    timeout         : timeout
                })

                if (this.codeProcessed) this.scheduleSpecsLaunch()

                this.sequentialSubTests.push(subTest)
            },

            /**
             * This method returns an "expectation" instance, which can be used to check various assertions about the passed value.
             *
             * **Note**, that every expectation has a special property `not`, that contains another expectation, but with the negated meaning.
             *
             * For example:
             *

        t.expect(1).toBe(1)
        t.expect(1).not.toBe(2)

        t.expect('Foo').toContain('oo')
        t.expect('Foo').not.toContain('bar')

             * Please refer to the documentation of the {@link Siesta.Test.BDD.Expectation} class for the list of available methods.
             *
             * @param {Mixed} value Any value, that will be assert about
             * @return {Siesta.Test.BDD.Expectation} Expectation instance
             */
            expect : function (value) {
                return new this.expectationClass({
                    t           : this,
                    value       : value
                })
            },

            /**
             * This method returns a *placeholder*, denoting any instance of the provided class constructor. Such placeholder can be used in various
             * comparison assertions, like {@link #is}, {@link #isDeeply}, {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
             * {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
             *
             * For example:

        t.is(1, t.any(Number))

        t.expect(1).toBe(t.any(Number))

        t.isDeeply({ name : 'John', age : 45 }, { name : 'John', age : t.any(Number))

        t.expect({ name : 'John', age : 45 }).toEqual({ name : 'John', age : t.any(Number))

        t.is(NaN, t.any(), 'When class constructor is not provided `t.any()` should match anything')

             *
             * See also {@link #anyNumberApprox}, {@link #anyStringLike}.
             *
             * @param {Function} clsConstructor A class constructor instances of which are denoted with this placeholder. As a special case if this argument
             * is not provided, a placeholder will match any value.
             *
             * @return {Object} A placeholder object
             */
            any : function (clsConstructor) {
                return new Siesta.Test.BDD.Placeholder({
                    clsConstructor      : clsConstructor,
                    t                   : this,
                    context             : this.global
                })
            },

            /**
             * This method returns a *placeholder*, denoting any number approximately equal to the provided value.
             * Such placeholder can be used in various comparison assertions, like {@link #is}, {@link #isDeeply},
             * {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
             * {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
             *
             * For example:

        t.is(1, t.anyNumberApprox(1.2, 0.5))

        t.expect(1).toBe(t.anyNumberApprox(1.2, 0.5))

             *
             * @param {Number} value The approximate value
             * @param {Number} [threshold] The threshold. If omitted, it is set to 5% from the `value`.
             *
             * @return {Object} A placeholder object
             */
            anyNumberApprox : function (value, threshold) {
                return new Siesta.Test.BDD.NumberPlaceholder({
                    value               : value,
                    threshold           : threshold
                })
            },

            /**
             * This method returns a *placeholder*, denoting any string that matches provided value.
             * Such placeholder can be used in various comparison assertions, like {@link #is}, {@link #isDeeply},
             * {@link Siesta.Test.BDD.Expectation#toBe expect().toBe()},
             * {@link Siesta.Test.BDD.Expectation#toBe expect().toEqual()} and so on.
             *
             * For example:

        t.is('foo', t.anyStringLike('oo'))

        t.expect('bar').toBe(t.anyStringLike(/ar$/))

             *
             * @param {String/RegExp} value If given as string will denote a substring a string being checked should contain,
             * if given as RegExp instance then string being checked should match this RegExp
             *
             * @return {Object} A placeholder object
             */
            anyStringLike : function (value) {
                return new Siesta.Test.BDD.StringPlaceholder({ value : value })
            },

            scheduleSpecsLaunch : function () {
                if (this.launchTimeout) return

                var async                   = this.beginAsync()
                var originalSetTimeout      = this.originalSetTimeout
                var me                      = this

                this.launchTimeout          = originalSetTimeout(function () {
                    me.endAsync(async)
                    me.launchTimeout        = null

                    me.launchSpecs()
                }, 0)
            },

            runBeforeSpecHooks : function (sourceTest, done) {
                var me          = this

                var runOwnHooks = function (done) {
                    me.chainForArray(me.beforeEachHooks, function (hook) {
                        return function (next) {
                            var code        = hook.code

                            if (hook.isAsync) {
                                code(sourceTest, next)
                            } else {
                                code(sourceTest)
                                next()
                            }
                        }
                    }, done)
                }

                if (this.parent)
                    this.parent.runBeforeSpecHooks(sourceTest, function () {
                        runOwnHooks(done)
                    })
                else
                    runOwnHooks(done)
            },

            runAfterSpecHooks : function (sourceTest, done) {
                var me      = this

                me.chainForArray(
                    this.afterEachHooks, function (hook) {
                        return function (next) {
                            var code        = hook.code

                            if (hook.isAsync) {
                                code(sourceTest, next)
                            } else {
                                code(sourceTest)
                                next()
                            }
                        }
                    }, function () {
                        me.parent ? me.parent.runAfterSpecHooks(sourceTest, done) : done()
                    },
                    // reverse
                    true
                )
            },

            launchSpecs : function () {
                var me                  = this
                var sequentialSubTests  = this.sequentialSubTests

                this.sequentialSubTests = []

                // hackish way to pass a config to `t.chain`
                this.chain.actionDelay  = 0

                var exclusiveSubTests   = []

                Joose.A.each(sequentialSubTests, function (subTest) {
                    if (subTest.isExclusive) exclusiveSubTests.push(subTest)
                })

                this.chainForArray(exclusiveSubTests.length ? exclusiveSubTests : sequentialSubTests, function (subTest) {
                    return [
                        subTest.specType == 'it' ? function (next) { me.runBeforeSpecHooks(subTest, next) } : null,
                        subTest,
                        subTest.specType == 'it' ? function (next) { me.runAfterSpecHooks(subTest, next) } : null
                    ]
                })
            },

            /**
             * This method allows you to execute some "setup" code hook before every spec ("it" block) of the current test.
             * Such hooks are **not** executed for the "describe" blocks and sub-tests generated with
             * the {@link Siesta.Test#getSubTest getSubTest} method.
             *
             * Note, that specs can be nested and all `beforeEach` hooks are executed in order, starting from the outer-most one.
             *
             * The hook function can be declared with 1 or 2 arguments. The 1st argument is always the test
             * instance being launched.
             *
             * If hook is declared with only 1 argument - it is supposed to be synchronous.
             *
             * If hook is declared with 2 arguments - it is supposed to be asynchronous (you can also force the asynchronous
             * mode with the `isAsync` argument, see below). The completion callback will be provided as the 2nd argument for the hook.
             *
             * This method can be called several times, providing several "hook" functions.
             *
             * For example:

        StartTest(function (t) {
            var baz     = 0

            t.beforeEach(function (t) {
                // the `t` instance here is the "t" instance from the "it" block below
                baz     = 0
            })

            t.it("This feature should work", function (t) {
                t.expect(myFunction(baz++)).toEqual('someResult')
            })
        })

             *
             * @param {Function} code A function to execute before every spec
             * @param {Siesta.Test} code.t A test instance being launched
             * @param {Function} code.next A callback to call when the `beforeEach` method completes. This argument is only provided
             * when hook function is declared with 2 arguments (or the `isAsync` argument is passed as `true`)
             * @param {Boolean} isAsync When passed as `true` this argument makes the `beforeEach` method asynchronous. In this case,
             * the `code` function will receive an additional callback argument, which should be called once the method has completed its work.
             *
             * Note, that `beforeEach` method should complete within {@link Siesta.Test#defaultTimeout defaultTimeout} time, otherwise
             * failing assertion will be added to the test.
             *
             * Example of asynchronous hook:

        StartTest(function (t) {
            var baz     = 0

            // asynchronous setup code
            t.beforeEach(function (t, next) {

                // `beforeEach` will complete in 100ms
                setTimeout(function () {
                    baz     = 0
                    next()
                }, 100)
            })

            t.describe("This feature should work", function (t) {
                t.expect(myFunction(baz++)).toEqual('someResult')
            })
        })

             */
            beforeEach : function (code, isAsync) {
                this.beforeEachHooks.push({ code : code, isAsync : isAsync || code.length == 2 })
            },

            /**
             * This method allows you to execute some "setup" code hook after every spec ("it" block) of the current test.
             * Such hooks are **not** executed for the "describe" blocks and sub-tests generated with
             * the {@link Siesta.Test#getSubTest getSubTest} method.
             *
             * Note, that specs can be nested and all `afterEach` hooks are executed in order, starting from the most-nested one.
             *
             * The hook function can be declared with 1 or 2 arguments. The 1st argument is always the test
             * instance being launched.
             *
             * If hook is declared with only 1 argument - it is supposed to be synchronous.
             *
             * If hook is declared with 2 arguments - it is supposed to be asynchronous (you can also force the asynchronous
             * mode with the `isAsync` argument, see below). The completion callback will be provided as the 2nd argument for the hook.
             *
             * This method can be called several times, providing several "hook" functions.
             *
             * For example:

        StartTest(function (t) {
            var baz     = 0

            t.afterEach(function (t) {
                // the `t` instance here is the "t" instance from the "it" block below
                baz     = 0
            })

            t.it("This feature should work", function (t) {
                t.expect(myFunction(baz++)).toEqual('someResult')
            })
        })

             *
             * @param {Function} code A function to execute after every spec
             * @param {Siesta.Test} code.t A test instance being completed
             * @param {Function} code.next A callback to call when the `afterEach` method completes. This argument is only provided
             * when hook function is declared with 2 arguments (or the `isAsync` argument is passed as `true`)
             * @param {Boolean} isAsync When passed as `true` this argument makes the `afterEach` method asynchronous. In this case,
             * the `code` function will receive an additional callback argument, which should be called once the method has completed its work.
             *
             * Note, that `afterEach` method should complete within {@link Siesta.Test#defaultTimeout defaultTimeout} time, otherwise
             * failing assertion will be added to the test.
             *
             * Example of asynchronous hook:

        StartTest(function (t) {
            var baz     = 0

            // asynchronous setup code
            t.afterEach(function (t, next) {

                // `afterEach` will complete in 100ms
                setTimeout(function () {
                    baz     = 0
                    next()
                }, 100)
            })

            t.describe("This feature should work", function (t) {
                t.expect(myFunction(baz++)).toEqual('someResult')
            })
        })

             */
            afterEach : function (code, isAsync) {
                this.afterEachHooks.push({ code : code, isAsync : isAsync || code.length == 2 })
            },

            /**
             * This method installs a "spy" instead of normal function in some object. The "spy" is basically another function,
             * which tracks the calls to itself. With spies, one can verify that some function was called and that
             * it was called with certain arguments.
             *
             * By default, spy will call the original method and return a value from it. To enable different behavior, you can use one of these methods:
             *
             * - {@link Siesta.Test.BDD.Spy#returnValue returnValue} - return a specific value
             * - {@link Siesta.Test.BDD.Spy#callThrough callThrough} - call the original method and return a value from it
             * - {@link Siesta.Test.BDD.Spy#stub stub} - call the original method and return a value from it
             * - {@link Siesta.Test.BDD.Spy#callFake callFake} - call the provided function and return a value from it
             * - {@link Siesta.Test.BDD.Spy#throwError throwError} - throw a specific exception object
             *

            const spy = t.spyOn(obj, 'process')
            // or, if you need to call some method instead
            const spy = t.spyOn(obj, 'process').and.callFake(() => {
                // is called instead of `process` method
            })

            // call the method
            obj.process('fast', 1)

            t.expect(spy).toHaveBeenCalled();
            t.expect(spy).toHaveBeenCalledWith('fast', 1);

             *
             * See also {@link #createSpy}, {@link #createSpyObj}, {@link Siesta.Test.BDD.Expectation#toHaveBeenCalled toHaveBeenCalled},
             * {@link Siesta.Test.BDD.Expectation#toHaveBeenCalledWith toHaveBeenCalledWith}
             *
             * See also the {@link Siesta.Test.BDD.Spy} class for additional details.
             *
             * @param {Object} object An object which property is being spied
             * @param {String} propertyName A name of the property over which to install the spy.
             *
             * @return {Siesta.Test.BDD.Spy} spy Created spy instance
             */
            spyOn : function (object, propertyName) {
                var R       = Siesta.Resource('Siesta.Test.BDD')

                if (!object) { this.warn(R.get('noObject')); return; }

                return new Siesta.Test.BDD.Spy({
                    name            : propertyName,

                    t               : this,
                    hostObject      : object,
                    propertyName    : propertyName
                })
            },

            /**
             * This method create a standalone spy function, which tracks all calls to it. Tracking is done using the associated
             * spy instance, which is available as `and` property. One can use the {@link Siesta.Test.BDD.Spy} class API to
             * verify the calls to the spy function.
             *
             * Example:

        var spyFunc     = t.createSpy('onadd listener')

        myObservable.addEventListener('add', spyFunc)

        // do something that triggers the `add` event on the `myObservable`

        t.expect(spyFunc).toHaveBeenCalled()

        t.expect(spyFunc.calls.argsFor(1)).toEqual([ 'Arg1', 'Arg2' ])

             *
             * See also: {@link #spyOn}
             *
             * @param {String} [spyName='James Bond'] A name of the spy for debugging purposes
             *
             * @return {Function} Created function. The associated spy instance is assigned to it as the `and` property
             */
            createSpy : function (spyName) {
                return (new Siesta.Test.BDD.Spy({
                    name            : spyName || 'James Bond',
                    t               : this
                })).stub().getProcessor()
            },

            /**
             * This method creates an object, which properties are spy functions. Such object can later be used as a mockup.
             *
             * This method can be called with one argument only, which should be an array of properties.
             *
             * Example:

        var mockup      = t.createSpyObj('encoder-mockup', [ 'encode', 'decode' ])
        // or just
        var mockup      = t.createSpyObj([ 'encode', 'decode' ])

        mockup.encode('string')
        mockup.decode('string')

        t.expect(mockup.encode).toHaveBeenCalled()

             *
             * See also: {@link #createSpy}
             *
             * @param {String} spyName A name of the spy object. Can be omitted.
             * @param {Array[String]} properties An array of the property names. For each property name a spy function will be created.
             *
             * @return {Object} A mockup object
             */
            createSpyObj : function (spyName, properties) {
                if (arguments.length == 1) { properties = spyName; spyName = null }

                spyName     = spyName || 'spyObject'

                var me      = this
                var obj     = {}

                Joose.A.each(properties, function (propertyName) {
                    obj[ propertyName ] = me.createSpy(spyName)
                })

                return obj
            }
        },

        override : {

            cleanup : function () {
                this.beforeEachHooks    = this.afterEachHooks = null

                this.SUPER()
            },

            onTestFinalize : function () {
                Joose.A.each(this.spies, function (spy) { spy.remove() })

                this.spies  = null

                this.SUPER()
            },

            afterLaunch : function () {
                this.codeProcessed      = true

                this.launchSpecs()

                this.SUPERARG(arguments)
            }
        }

    })
    //eof Siesta.Test.BDD