    /*

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

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

    This class implements a "spy" - function wrapper which tracks the calls to itself. Spy can be installed
    instead of a method in some object or can be used standalone.

    Note, that spies "belongs" to a spec and once the spec is completed all spies that were installed during it
    will be removed.

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

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

        has         : {
            name                    : null,

            processor               : {
                lazy        : 'this.buildProcessor'
            },

            hostObject              : null,
            propertyName            : null,

            hasOwnOriginalValue     : false,
            originalValue           : null,

            strategy                : 'callThrough',

            returnValueObj          : undefined,
            fakeFunc                : null,
            throwErrorObj           : null,

            // array of { object : scope, args : [], returnValue : }
            callsLog                : Joose.I.Array,

            /**
             * @property {Object} calls This is an object property with several helper methods, related to the calls
             * tracking information. It is assigned to the wrapper function of spy.
             *
             * @property {Function} calls.any Returns `true` if spy was called at least once, `false` otherwise
             * @property {Function} calls.count Returns the number of times this spy was called
             * @property {Function} calls.argsFor Accepts an number of the call (0-based) and return an array of arguments
             * for that call.
             * @property {Function} calls.allArgs Returns an array with the arguments for every tracked function call.
             * Every element of the array is, in turn, an array of arguments.
             * @property {Function} calls.all Returns an array with the context for every tracked function call.
             * Every element of the array is an object of the following structure:

        { object : this, args : [ 0, 1, 2 ], returnValue : undefined }

             * @property {Function} calls.mostRecent Returns a context object of the most-recent tracked function call.
             * @property {Function} calls.first Returns a context object of the first tracked function call.
             * @property {Function} calls.reset Reset all tracking data.
             *
             *
             * Example:

        t.spyOn(obj, 'someMethod').callThrough()

        obj.someMethod(0, 1)
        obj.someMethod(1, 2)

        t.expect(obj.someMethod.calls.any()).toBe(true)
        t.expect(obj.someMethod.calls.count()).toBe(2)
        t.expect(obj.someMethod.calls.first()).toEqual({ object : obj, args : [ 0, 1 ], returnValue : undefined })

             */
            calls                   : null,

            t                       : null,

            /**
             * @property {Siesta.Test.BDD.Spy} and This is just a reference to itself, to add some syntax sugar.
             *
             * This property is also assigned to the wrapper function of spy.
             *

        t.spyOn(obj, 'someMethod').callThrough()

        // same thing as above
        t.spyOn(obj, 'someMethod').and.callThrough()

        // returns spy instance
        obj.someMethod.and

             */
            and                     : function () { return this }
        },

        methods     : {

            initialize : function () {
                var me              = this

                this.calls          = {
                    any         : function () { return me.callsLog.length > 0 },
                    count       : function () { return me.callsLog.length },
                    argsFor     : function (i) { return me.callsLog[ i ].args },

                    allArgs     : function (i) { return Joose.A.map(me.callsLog, function (call) { return call.args } ) },
                    all         : function () { return me.callsLog },

                    mostRecent  : function () { return me.callsLog[ me.callsLog.length - 1 ] },
                    first       : function () { return me.callsLog[ 0 ] },

                    reset       : function () { me.reset() }
                }

                var R       = Siesta.Resource('Siesta.Test.BDD.Spy')

                var hostObject      = this.hostObject
                var propertyName    = this.propertyName

                if (hostObject) {
                    var originalValue           = hostObject[ propertyName ]

                    if (!/Function/.test(this.typeOf(originalValue))) throw R.get("spyingNotOnFunction")

                    if (originalValue.__SIESTA_SPY__) originalValue.__SIESTA_SPY__.remove()

                    this.originalValue          = hostObject[ propertyName ]
                    this.hasOwnOriginalValue    = hostObject.hasOwnProperty(propertyName)

                    hostObject[ propertyName ]  = this.getProcessor()
                }

                if (this.t) this.t.spies.push(this)
            },

            buildProcessor : function () {
                var me          = this

                var processor   = function () {
                    var args        = Array.prototype.slice.call(arguments)
                    var log         = { object : this, args : args }

                    me.callsLog.push(log)

                    return log.returnValue = me[ me.strategy + 'Strategy' ](this, args)
                }

                processor.__SIESTA_SPY__    = processor.and = me
                processor.calls             = me.calls

                return processor
            },

            returnValueStrategy : function (obj, args) {
                return this.returnValueObj
            },

            callThroughStrategy : function (obj, args) {
                return this.originalValue.apply(obj, args)
            },

            callFakeStrategy : function (obj, args) {
                return this.fakeFunc.apply(obj, args)
            },

            throwErrorStrategy : function (obj, args) {
                var error       = this.throwErrorObj
                var ERROR       = this.t && this.t.global ? this.t.global.Error : Error

                if (!(error instanceof ERROR)) error = new ERROR(error)

                throw error
            },

            /**
             * This method makes the spy to also execute the original function it has been installed over. The
             * value returned from original function is returned from the spy.
             *
             * @return {Siesta.Test.BDD.Spy} This spy instance
             */
            callThrough : function () {
                if (!this.hostObject) throw "Need the host object to call through to original method"

                this.strategy       = 'callThrough'

                return this
            },

            /**
             * This method makes the spy to just return `undefined` and not execute the original function.
             *
             * @return {Siesta.Test.BDD.Spy} This spy instance
             */
            stub : function () {
                this.returnValue()

                return this
            },

            /**
             * This method makes the spy to return the value provided and not execute the original function.
             *
             * @param {Object} value The value that will be returned from the spy.
             *
             * @return {Siesta.Test.BDD.Spy} This spy instance
             */
            returnValue : function (value) {
                this.strategy       = 'returnValue'

                this.returnValueObj = value

                return this
            },

            /**
             * This method makes the spy to call the provided function and return the value from it, instead of the original function.
             *
             * @param {Function} func The function to call instead of the original function
             *
             * @return {Siesta.Test.BDD.Spy} This spy instance
             */
            callFake : function (func) {
                this.strategy   = 'callFake'

                this.fakeFunc   = func

                return this
            },

            /**
             * This method makes the spy to throw the specified `error` value (instead of calling the original function).
             *
             * @param {Object} error The error value to throw. If it is not an `Error` instance, it will be passed to `Error` constructor first.
             *
             * @return {Siesta.Test.BDD.Spy} This spy instance
             */
            throwError : function (error) {
                this.strategy       = 'throwError'

                this.throwErrorObj  = error

                return this
            },

            remove : function () {
                var hostObject      = this.hostObject

                if (hostObject) {
                    if (this.hasOwnOriginalValue)
                        hostObject[ this.propertyName ] = this.originalValue
                    else
                        delete hostObject[ this.propertyName ]
                }

                // cleanup paranoya
                this.originalValue  = this.hostObject = hostObject = null
                this.callsLog       = []

                this.returnValueObj = this.fakeFunc = this.throwErrorObj = null

                var processor       = this.getProcessor()

                if (processor)
                    processor.and   = processor.calls   = processor.__SIESTA_SPY__ = null

                this.processor      = null
            },

            /**
             * This method resets all calls tracking data. Spy will report as it has never been called yet.
             */
            reset : function () {
                this.callsLog      = []
            }
        }
    })