    /*

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

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

    This class is the central point for writing assertions in BDD style. Instances of this class can be generated with the {@link Siesta.Test#expect expect}
    method. Then, calling some method on the instance will create a new assertion in the test.

    * **Note**, that to negate any assertion, you can use a special property {@link #not}, that contains an expectation instance with the opposite 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')

    */
    Class('Siesta.Test.BDD.Expectation', {

        does        : [
            Siesta.Util.Role.CanGetType
        ],

        has         : {
            value           : null,

            isNot           : false,

            /**
             * @property {Siesta.Test.BDD.Expectation} not Another expectation instance with the negated meaning.
             */
            not             : null,

            t               : null
        },

        methods     : {

            initialize : function () {

                if (!this.isNot) this.not = new this.constructor({
                    isNot           : true,
                    t               : this.t,

                    value           : this.value
                })
            },

            process : function (passed, config) {
                var isNot       = this.isNot
                config          = config || {}

                config.not      = config.not || isNot ? 'not ' : ''
                config.got      = config.hasOwnProperty('got') ? config.got : this.value

                if (config.noGot) delete config.got

                var assertionName   = config.assertionName

                if (assertionName && isNot) config.assertionName = assertionName.replace(/^(expect\(.+?\)\.)/, '$1not.')

                passed          = isNot ? !passed : passed

                this.t[ passed ? 'pass' : 'fail' ](null, config)
            },

            /**
             * This assertion compares the value provided to the {@link Siesta.Test#expect expect} method with the `expectedValue` argument.
             * Comparison is done with `===` operator, so it should be used **only with the primitivies** - numbers, strings, booleans etc.
             *
             * To deeply compare Date, Arrays and JSON objects in general, use {@link #toEqual} method.
             *
             * This method works correctly with the placeholders generated with {@link Siesta.Test#any any} method
             *
             * @param {Primitive} expectedValue An expected value
             */
            toBe : function (expectedValue) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.t.compareObjects(this.value, expectedValue, true, true), {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' {need}',
                    assertionName       : 'expect(got).toBe(need)',
                    need                : expectedValue,
                    needDesc            : this.isNot ? R.get('needNotText') : R.get('needText')
                })
            },

            /**
             * This assertion compares the value provided to the {@link Siesta.Test#expect expect} method with the `expectedValue` argument.
             *
             * Comparison works for Date, Array, and JSON objects in general. It is performed "deeply".
             * Right now the values should not contain cyclic references.
             *
             * This method works correctly with the placeholders generated with {@link Siesta.Test#any any} method
             *
             * @param {Mixed} expectedValue An expected value
             */
            toEqual : function (expectedValue) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.t.compareObjects(this.value, expectedValue, true), {
                    descTpl             : R.get('expectText') +' {got} {!not}' + R.get('toBeEqualToText') + ' {need}',
                    assertionName       : 'expect(got).toEqual(need)',
                    need                : expectedValue,
                    needDesc            : this.isNot ? R.get('needNotText') : R.get('needText')
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is `null`.
             */
            toBeNull : function () {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.t.compareObjects(this.value, null, true, true), {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' null',
                    assertionName       : 'expect(got).toBeNull()',
                    need                : null,
                    needDesc            : this.isNot ? R.get('needNotText') : R.get('needText')
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is `NaN`.
             */
            toBeNaN : function () {
                var value   = this.value
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.t.typeOf(value) == 'Number' && value != value, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeText') + ' NaN',
                    assertionName       : 'expect(got).toBeNaN()',
                    need                : NaN,
                    needDesc            : this.isNot ? R.get('needNotText') : R.get('needText')
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is not the `undefined` value.
             */
            toBeDefined : function () {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.value !== undefined, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeDefinedText'),
                    assertionName       : 'expect(got).toBeDefined()'
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is the `undefined` value.
             */
            toBeUndefined : function (value) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.value === undefined, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeUndefinedText'),
                    assertionName       : 'expect(got).toBeUndefined()'
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is "truthy" - evaluates to `true`.
             * For example - non empty strings, numbers except the 0, objects, arrays etc.
             */
            toBeTruthy : function () {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.value, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeTruthyText'),
                    assertionName       : 'expect(got).toBeTruthy()'
                })
            },

            /**
             * This assertion passes, when value provided to the {@link Siesta.Test#expect expect} method is "falsy" - evaluates to `false`.
             * For example - empty strings, number 0, `null`, `undefined`, etc.
             */
            toBeFalsy : function () {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(!this.value, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeFalsyText'),
                    assertionName       : 'expect(got).toBeFalsy()'
                })
            },

            /**
             * This assertion passes, when the string provided to the {@link Siesta.Test#expect expect} method matches the regular expression.
             *
             * @param {RegExp} regexp The regular expression to match the string against
             */
            toMatch : function (regexp) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                if (this.t.typeOf(regexp) != 'RegExp') throw new Error("`expect().toMatch()` matcher expects a regular expression")

                this.process(new RegExp(regexp).test(this.value), {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toMatchText') + ' {need}',
                    assertionName       : 'expect(got).toMatch(need)',
                    need                : regexp,
                    needDesc            : this.isNot ? R.get('needNotMatchingText') : R.get('needMatchingText')
                })
            },

            /**
             * This assertion passes in 2 cases:
             *
             * 1) When the value provided to the {@link Siesta.Test#expect expect} method is a string, and it contains a passed substring.
             * 2) When the value provided to the {@link Siesta.Test#expect expect} method is an array (or array-like), and it contains a passed element.
             *
             * @param {String/Mixed} element The element of the array or a sub-string
             */
            toContain : function (element) {
                var value       = this.value
                var t           = this.t
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                var passed      = false

                if (t.typeOf(value) == 'String') {
                    this.process(value.indexOf(element) >= 0, {
                        descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toContainText') + ' {need}',
                        assertionName       : 'expect(got).toContain(need)',
                        need                : element,
                        needDesc            : this.isNot ? R.get('needStringNotContainingText') : R.get('needStringContainingText')
                    })
                } else {
                    // Normalize to allow NodeList, Arguments etc.
                    value = Array.prototype.slice.call(value);

                    for (var i = 0; i < value.length; i++)
                        if (t.compareObjects(element, value[ i ], true)) {
                            passed      = true
                            break
                        }

                    this.process(passed, {
                        descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toContainText') + ' {need}',
                        assertionName       : 'expect(got).toContain(need)',
                        need                : element,
                        needDesc            : this.isNot ? R.get('needArrayNotContainingText') : R.get('needArrayContainingText')
                    })

                }
            },

            /**
             * This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is less than the
             * expected number.
             *
             * @param {Number} expectedValue The number to compare with
             */
            toBeLessThan : function (expectedValue) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.value < expectedValue, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeLessThanText') + ' {need}',
                    assertionName       : 'expect(got).toBeLessThan(need)',
                    need                : expectedValue,
                    needDesc            : this.isNot ? R.get('needGreaterEqualThanText') : R.get('needLessThanText')
                })
            },

            /**
             * This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is greater than the
             * expected number.
             *
             * @param {Number} expectedValue The number to compare with
             */
            toBeGreaterThan : function (expectedValue) {
                var R = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(this.value > expectedValue, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeGreaterThanText') + ' {need}',
                    assertionName       : 'expect(got).toBeGreaterThan(need)',
                    need                : expectedValue,
                    needDesc            : this.isNot ? R.get('needLessEqualThanText') : R.get('needGreaterThanText')
                })
            },

            /**
             * This assertion passes, when the number provided to the {@link Siesta.Test#expect expect} method is approximately equal
             * the given number. The proximity can be defined as the `precision` argument
             *
             * @param {Number} expectedValue The number to compare with
             * @param {Number} [precision=2] The number of digits after dot (comma) that should be same in both numbers.
             */
            toBeCloseTo : function (expectedValue, precision) {
                precision       = precision != null ? precision : 2

                // not sure why we divide the precision by 2, but jasmine does that for some reason
                var threshold   = Math.pow(10, -precision) / 2
                var delta       = Math.abs(this.value - expectedValue)
                var R           = Siesta.Resource('Siesta.Test.BDD.Expectation');

                this.process(delta < threshold, {
                    descTpl             : R.get('expectText') + ' {got} {!not}' + R.get('toBeCloseToText') +' {need}',
                    assertionName       : 'expect(got).toBeCloseTo(need)',
                    need                : expectedValue,
                    needDesc            : this.isNot ? R.get('needValueNotCloseToText') : R.get('needValueCloseToText'),
                    annotation          : delta ? R.get('thresholdIsText') + threshold : R.get('exactMatchText')
                })
            },

            /**
             * This assertion passes when the function provided to the {@link Siesta.Test#expect expect} method, throws an exception
             * during its execution.
             *
             * t.expect(function(){
             *     throw "oopsie";
             * }).toThrow());
             *
             */
            toThrow : function () {
                var func    = this.value
                var t       = this.t
                var R       = Siesta.Resource('Siesta.Test.BDD.Expectation');

                if (t.typeOf(func) != 'Function') throw new Error("`expect().toMatch()` matcher expects a function")

                var e       = t.getExceptionCatcher()(func)

                if (e instanceof t.getTestErrorClass())
                    //IE uses non-standard 'description' property for error msg
                    e = e.message || e.description

                this.process(e !== undefined, {
                    descTpl             : R.get('expectText') + ' function {!not}' + R.get('toThrowText'),
                    assertionName       : 'expect(func).toThrow()',
                    annotation          : e ? (R.get('thrownExceptionText') + ': ' + Siesta.Util.Serializer.stringify(e)) : R.get('noExceptionThrownText'),

                    noGot               : true
                })
            },

            /**
             * This assertion passes, if a spy, provided to the {@link Siesta.Test#expect expect} method have been
             * called expected number of times. The expected number of times can be provided as the 1st argument and by default
             * is 1.
             *
             * One can also provide the function, spied on, to the {@link Siesta.Test#expect expect} method.
             *
             * Examples:
             *
        var spy = t.spyOn(obj, 'process')

        // call the method 2 times
        obj.process()
        obj.process()

        // following 2 calls are equivalent
        t.expect(spy).toHaveBeenCalled();
        t.expect(obj.process).toHaveBeenCalled();

        // one can also use exact number of calls or comparison operators
        t.expect(obj.process).toHaveBeenCalled(2);
        t.expect(obj.process).toHaveBeenCalled('>1');
        t.expect(obj.process).toHaveBeenCalled('<=3');

             *
             * See also {@link #toHaveBeenCalledWith}
             *
             * @param {Number/String} expectedNumber Expected number of calls. Can be either a number, specifying the exact
             * number of calls, or a string. In the latter case one can include a comparison operator in front of the number.
             *
             */
            toHaveBeenCalled : function (expectedNumber) {
                expectedNumber  = expectedNumber != null ? expectedNumber : '>=1'

                var spy         = this.value
                var t           = this.t
                var R           = Siesta.Resource('Siesta.Test.BDD.Expectation');

                if (this.typeOf(spy) == 'Function') {
                    if (!spy.__SIESTA_SPY__) throw new Error(R.get('wrongSpy'))

                    spy         = spy.__SIESTA_SPY__
                }

                if (!(spy instanceof Siesta.Test.BDD.Spy)) throw new Error(R.get('wrongSpy'))

                this.process(t.verifyExpectedNumber(spy.callsLog.length, expectedNumber), {
                    descTpl             : R.get('toHaveBeenCalledDescTpl'),
                    assertionName       : 'expect(func).toHaveBeenCalled()',
                    methodName          : spy.propertyName || '[function]',
                    got                 : spy.callsLog.length,
                    gotDesc             : R.get('actualNbrOfCalls'),
                    need                : (this.isNot ? 'not ' : '') + expectedNumber,
                    needDesc            : R.get('expectedNbrOfCalls')
                })
            },

            /**
             * This assertion passes, if a spy, provided to the {@link Siesta.Test#expect expect} method have been
             * called at least once with the specified arguments.
             *
             * One can also provide the function, spied on, to the {@link Siesta.Test#expect expect} method.
             *
             * One can use placeholders, generated with the {@link Siesta.Test.BDD#any any} method to verify the arguments.
             *
             * Example:
             *

        var spy = t.spyOn(obj, 'process')

        // call the method 2 times with different arguments
        obj.build('development', '1.0.0')
        obj.build('release', '1.0.1')

        t.expect(spy).toHaveBeenCalledWith('development', '1.0.0');
        // or
        t.expect(obj.process).toHaveBeenCalledWith('development', t.any(String));

             *
             * See also {@link #toHaveBeenCalled}
             *
             * @param {Object} arg1 Argument to a call
             * @param {Object} arg2 Argument to a call
             * @param {Object} argN Argument to a call
             */
            toHaveBeenCalledWith : function () {
                var spy         = this.value
                var t           = this.t
                var R           = Siesta.Resource('Siesta.Test.BDD.Expectation');

                if (this.typeOf(spy) == 'Function') {
                    if (!spy.__SIESTA_SPY__) throw new Error(R.get('wrongSpy'))

                    spy         = spy.__SIESTA_SPY__
                }

                if (!(spy instanceof Siesta.Test.BDD.Spy)) throw new Error(R.get('wrongSpy'))

                var args                        = Array.prototype.slice.call(arguments)
                var foundCallWithMatchingArgs   = false

                Joose.A.each(spy.callsLog, function (call) {
                    if (t.compareObjects(call.args, args)) { foundCallWithMatchingArgs = true; return false }
                })

                this.process(foundCallWithMatchingArgs, {
                    descTpl             : R.get('toHaveBeenCalledWithDescTpl'),
                    assertionName       : 'expect(func).toHaveBeenCalledWith()',
                    methodName          : spy.propertyName,
                    noGot               : true
                })
            }
        }
    })