/*

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

*/
/**
@class Siesta.Test.Observable

This is a mixin, with assertions/ helper methods for testing observable pattern (in NodeJS world known as `EventEmitter`).

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

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

    requires : [
        'addListenerToObservable',
        'removeListenerFromObservable',
        'getSourceLine',
        'pass', 'fail',
        'processCallbackFromTest'
    ],

    has : {
    },

    methods : {

        /**
         * This method will wait for the first browser `event`, fired by the provided `observable` and will then call the provided callback.
         *
         * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector.
         * @param {String} event The name of the event to wait for
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForEvent : function (observable, event, callback, scope, timeout) {
            var eventFired      = false
            var R               = Siesta.Resource('Siesta.Test.Browser');

            this.addListenerToObservable(observable, event, function () { eventFired = true })

            return this.waitFor({
                method          : function() { return eventFired; },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForEvent',
                description     : ' ' + R.get('waitForEvent') + ' "' + event + '" ' + R.get('event')
            });
        },


        /**
         * This assertion verifies the number of certain events fired by provided observable instance during provided period.
         *
         * For example:
         *

    t.firesOk({
        observable      : store,
        events          : {
            update      : 1,
            add         : 2,
            datachanged : '> 1'
        },
        during          : function () {
            store.getAt(0).set('Foo', 'Bar');

            store.add({ FooBar : 'BazQuix' })
            store.add({ Foo : 'Baz' })
        },
        desc            : 'Correct events fired'
    })

    // or

    t.firesOk({
        observable      : store,
        events          : {
            update      : 1,
            add         : 2,
            datachanged : '>= 1'
        },
        during          : 1
    })

    store.getAt(0).set('Foo', 'Bar');

    store.add({ FooBar : 'BazQuix' })
    store.add({ Foo : 'Baz' })

         *
         * Normally this method accepts a single object with various options (as shown above), but also can be called in 2 additional shortcuts forms:
         *

    // 1st form for multiple events
    t.firesOk(observable, { event1 : 1, event2 : '>1' }, description)

    // 2nd form for single event
    t.firesOk(observable, eventName, 1, description)
    t.firesOk(observable, eventName, '>1', description)

         *
         * In both forms, `during` is assumed to be undefined and `description` is optional.
         *
         * @param {Object} options An obect with the following properties:
         * @param {Ext.util.Observable/Ext.Element/HTMLElement} options.observable The observable instance that will fire events
         * @param {Object} options.events The object, properties of which corresponds to event names and values - to expected
         * number of this event triggering. If value of some property is a number then exact that number of events is expected. If value
         * of some property is a string starting with one of the comparison operators like "\<", "\<=", "==" etc and followed by the number
         * then Siesta will perform that comparison with the number of actualy fired events.
         * @param {Number/Function} [options.during] If provided as a number denotes the number of milliseconds during which
         * this assertion will "record" the events from observable, if provided as function - then this assertion will "record"
         * only events fired during execution of this function. If not provided at all - assertions are recorded until the end of
         * current test (or sub-test)
         * @param {Function} [options.callback] A callback to call after this assertion has been checked. Only used if `during` value is provided.
         * @param {String} [options.desc] A description for this assertion
         */
        firesOk: function (options, events, n, timeOut, func, desc, callback) {
            //                    |        backward compat arguments        |
            var me              = this;
            var sourceLine      = me.getSourceLine();
            var R               = Siesta.Resource('Siesta.Test.Browser');
            var nbrArgs         = arguments.length
            var observable, during

            if (nbrArgs == 1) {
                observable      = options.observable
                events          = options.events
                during          = options.during
                desc            = options.desc || options.description
                callback        = options.callback

                timeOut         = this.typeOf(during) == 'Number' ? during : null
                func            = this.typeOf(during) == 'Function' ? during : null

            } else if (nbrArgs >= 5) {
                // old signature, backward compat
                observable      = options

                if (this.typeOf(events) == 'String') {
                    var obj         = {}
                    obj[ events ]   = n

                    events          = obj
                }
            } else if (nbrArgs <= 3 && this.typeOf(events) == 'Object') {
                // shortcut form 1
                observable      = options
                desc            = n
            } else if (nbrArgs <= 4 && this.typeOf(events) == 'String') {
                // shortcut form 2
                observable      = options

                var obj         = {}
                obj[ events ]   = n
                events          = obj

                desc            = timeOut
                timeOut         = null
            } else
                throw new Error(R.get('unrecognizedSignature'))

            // start recording
            var counters    = {};
            var countFuncs  = {};

            Joose.O.each(events, function (expected, eventName) {
                counters[ eventName ]   = 0

                var countFunc   = countFuncs[ eventName ] = function () {
                    counters[ eventName ]++
                }

                me.addListenerToObservable(observable, eventName, countFunc);
            })


            // stop recording and verify the results
            var stopRecording   = function () {
                Joose.O.each(events, function (expected, eventName) {
                    me.removeListenerFromObservable(observable, eventName, countFuncs[ eventName ]);

                    var actualNumber    = counters[ eventName ]

                    if (me.verifyExpectedNumber(actualNumber, expected))
                        me.pass(desc, {
                            descTpl         : R.get('observableFired') + ' ' + actualNumber + ' `' + eventName + '` ' + R.get('events')
                        });
                    else
                        me.fail(desc, {
                            assertionName   : 'firesOk',
                            sourceLine      : sourceLine,
                            descTpl         : R.get('observableFiredOk') + ' `' + eventName + '` ' + R.get('events'),
                            got             : actualNumber,
                            gotDesc         : R.get('actualNbrEvents'),
                            need            : expected,
                            needDesc        : R.get('expectedNbrEvents')
                        });
                })
            }

            if (timeOut) {
                var async               = this.beginAsync(timeOut + 100);

                var originalSetTimeout  = this.originalSetTimeout;

                originalSetTimeout(function () {
                    me.endAsync(async);

                    stopRecording()

                    me.processCallbackFromTest(callback);
                }, timeOut);
            } else if (func) {
                func()

                stopRecording()

                me.processCallbackFromTest(callback)
            } else {
                this.on('beforetestfinalizeearly', stopRecording)
            }
        },


        /**
         * This assertion passes if the observable fires the specified event exactly (n) times during the test execution.
         *
         * @param {Ext.util.Observable/Ext.Element/HTMLElement} observable The observable instance
         * @param {String} event The name of event
         * @param {Number} n The expected number of events to be fired
         * @param {String} [desc] The description of the assertion.
         */
        willFireNTimes: function (observable, event, n, desc, isGreaterEqual) {
            this.firesOk(observable, event, isGreaterEqual ? '>=' + n : n, desc)
        },


        getObjectWithExpectedEvents : function (event, expected) {
            var events      = {}

            if (this.typeOf(event) == 'Array')
                Joose.A.each(event, function (eventName) {
                    events[ eventName ] = expected
                })
            else
                events[ event ]         = expected

            return events
        },


        /**
         * This assertion passes if the observable does not fire the specified event(s) after calling this method.
         *
         * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector.
         * @param {String/Array[String]} event The name of event or array of such
         * @param {String} [desc] The description of the assertion.
         */
        wontFire : function(observable, event, desc) {
            this.firesOk({
                observable      : observable,
                events          : this.getObjectWithExpectedEvents(event, 0),
                desc            : desc
            });
        },

        /**
         * This assertion passes if the observable fires the specified event exactly once after calling this method.
         *
         * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector.
         * @param {String/Array[String]} event The name of event or array of such
         * @param {String} [desc] The description of the assertion.
         */
        firesOnce : function(observable, event, desc) {
            this.firesOk({
                observable      : observable,
                events          : this.getObjectWithExpectedEvents(event, 1),
                desc            : desc
            });
        },

        /**
         * Alias for {@link #wontFire} method
         *
         * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector.
         * @param {String/Array[String]} event The name of event or array of such
         * @param {String} [desc] The description of the assertion.
         */
        isntFired : function() {
            this.wontFire.apply(this, arguments);
        },

        /**
         * This assertion passes if the observable fires the specified event at least `n` times after calling this method.
         *
         * @param {Mixed} observable Any browser observable, window object, element instances, CSS selector.
         * @param {String} event The name of event
         * @param {Number} n The minimum number of events to be fired
         * @param {String} [desc] The description of the assertion.
         */
        firesAtLeastNTimes : function(observable, event, n, desc) {
            this.firesOk(observable, event, '>=' + n, desc);
        },

        /**
         * This assertion will verify that the observable fires the specified event and supplies the correct parameters to the listener function.
         * A checker method should be supplied that verifies the arguments passed to the listener function, and then returns true or false depending on the result.
         * If the event was never fired, this assertion fails. If the event is fired multiple times, all events will be checked, but
         * only one pass/fail message will be reported.
         *
         * For example:
         *

    t.isFiredWithSignature(store, 'add', function (store, records, index) {
        return (store instanceof Ext.data.Store) && (records instanceof Array) && t.typeOf(index) == 'Number'
    })

         * @param {Ext.util.Observable/Siesta.Test.ActionTarget} observable Ext.util.Observable instance or target as specified by the {@link Siesta.Test.ActionTarget} rules with
         * the only difference that component queries will be resolved till the component level, and not the DOM element.
         * @param {String} event The name of event
         * @param {Function} checkerFn A method that should verify each argument, and return true or false depending on the result.
         * @param {String} [desc] The description of the assertion.
         */
        isFiredWithSignature : function(observable, event, checkerFn, description) {
            var eventFired;
            var me              = this;
            var sourceLine      = me.getSourceLine();
            var R               = Siesta.Resource('Siesta.Test.ExtJS.Observable');

            var verifyFiredFn = function () {
                me.removeListenerFromObservable(observable, event, listener)

                if (!eventFired) {
                    me.fail('The [' + event + "] " + R.get('isFiredWithSignatureNotFired'));
                }
            };

            me.on('beforetestfinalizeearly', verifyFiredFn);

            var listener = function () {
                me.un('beforetestfinalizeearly', verifyFiredFn);

                var result = checkerFn.apply(me, arguments);

                if (!eventFired && result) {
                    me.pass(description || R.get('observableFired') + ' ' + event + ' ' + R.get('correctSignature'), {
                        sourceLine  : sourceLine
                    });
                }

                if (!result) {
                    me.fail(description || R.get('observableFired') + ' ' + event + ' ' + R.get('incorrectSignature'), {
                        sourceLine  : sourceLine
                    });

                    // Don't spam the assertion grid with failure, one failure is enough
                    me.removeListenerFromObservable(observable, event, listener)
                }
                eventFired = true
            };

            me.addListenerToObservable(observable, event, listener)
        }
    }
});