describe("Ext.direct.RemotingProvider", function() {
    var RP = Ext.direct.RemotingProvider,
        provider,
        responseText,
        
        api = {
            actions: {
                "TestAction": [{
                    len:  1,
                    name: "echo"
                },{
                    len:  1,
                    name: "directFail"
                }, {
                    name: 'directForm',
                    formHandler: true
                }, {
                    len: 1,
                    name: 'directMetaNamed',
                    metadata: {
                        params: [],
                        strict: false
                    }
                }, {
                    len: 1,
                    name: 'directMetaOrdered',
                    metadata: {
                        len: 2
                    }
                }, {
                    name: 'directMetaFormNamed',
                    formHandler: true,
                    metadata: {
                        params: [],
                        strict: false
                    }
                }, {
                    name: 'directMetaFormOrdered',
                    formHandler: true,
                    metadata: {
                        len: 2
                    }
                }],
                
                "TestAction.Foo": [{
                    len:  0,
                    name: "foo"
                }],
                
                "TestAction.Foo.Bar": [{
                    len:  0,
                    name: "bar"
                }],
                
                "TestAction.Foo.Bar.Baz": [{
                    len:  0,
                    name: "baz"
                }],
                
                "TestAction.Foo.Qux": [{
                    len:  0,
                    name: "qux"
                }]
            },
            
            namespace: "Direct.foo.bar",
            type: "remoting",
            url: "/router",
            id: "foo"
        },
        
        directMethods = {
            echo: function(value) {
                return value;
            },
            
            directFail: function(value) {
                return {
                    type: 'exception'
                };
            },
            
            directForm: function(form) {
                return {
                    success: true,
                    data: form
                };
            },
            
            directMetaNamed: function(data, metadata) {
                return {
                    data: data,
                    metadata: metadata
                }
            },
            
            directMetaOrdered: function(data, metadata) {
                return {
                    data: data,
                    metadata: metadata
                }
            },
            
            directMetaFormNamed: function(form, metadata) {
                return {
                    success: true,
                    data: form,
                    metadata: metadata
                }
            },
            
            directMetaFormOrdered: function(form, metadata) {
                return {
                    success: true,
                    data: form,
                    metadata: metadata
                }
            },
            
            foo: function() {
                return 'foo';
            },
            
            bar: function() {
                return 'bar';
            },
            
            baz: function() {
                return 'baz';
            },
            
            qux: function() {
                return 'qux';
            }
        };
    
    // This simulation stub is *asynchronous* to allow call batching
    function simulateDirectRequest(options) {
        var callback = options.callback,
            scope = options.scope,
            transaction = options.transaction,
            isForm = options.form !== undefined,
            isUpload = options.isUpload,
            arg = {},
            success = true, // *Communication* success, i.e. connected to "server"
            data, tid, action, method, arg, fn, success,
            result, response, xhr, opt, metadata;
        
        if (isForm) {
            data     = options.params;
            tid      = data.extTID;
            action   = data.extAction;
            method   = data.extMethod;
            metadata = data.extMetadata;
            
            // Collect the input field values
            Ext.fly(options.form).select('input').each(function(el, c, idx) { 
                this[el.dom.name] = el.dom.value;
            }, arg);
            arg = [arg];
        }
        else {
            data     = options.jsonData;
            tid      = data.tid;
            action   = data.action;
            method   = data.method;
            arg      = data.data || [];
            metadata = data.metadata;
        }
        
        fn = directMethods[method];
        
        // TODO Come up with something less hacky
        if (/^directMeta/.test(method)) {
            arg.push(metadata);
        }
        
        if (options.timeout === 666) {
            response = {
                type: 'exception',
                tid: tid,
                message: "Can't connect to the server"
            };
            
            // Not connected
            success = false;
        }
        else if (responseText == null) {
            try {
                result   = fn.apply({}, arg);
                response = {
                    type: 'rpc',
                    tid: tid,
                    action: action,
                    method: method,
                    result: result
                };
            }
            catch (e) {
                // Direct exception handling here
                response = {
                    type: 'exception',
                    tid: tid,
                    message: e.toString(),
                    where: 'internal'
                };
            }
        }
        
        xhr = {
            responseText: responseText != null ? responseText : Ext.encode(response)
        };
        
        opt = {
            transaction: transaction
        };
        
        Ext.callback(callback, scope, [opt, success, xhr], 1);
    }
        
    beforeEach(function() {
        provider = new RP(api);
        provider.maxRetries = 0;
    });
    
    afterEach(function() {
        if (provider) {
            provider.destroy();
        }
        
        Ext.direct.Manager.clearAllMethods();
        
        provider = responseText = null;
        
        try {
            delete window.Direct;
        }
        catch (e) {
            window.Direct = undefined;
        }
    });
    
    describe("handles namespaces:", function() {
        var ns;
        
        it("creates namespace for itself if passed a string", function() {
            expect(Direct.foo.bar).toBeDefined();
        });
        
        it("doesn't create nested objects until it's connected", function() {
            expect(Direct.foo.bar).toEqual({});
        });
        
        describe("creates nested namespaces after it's connected:", function() {
            beforeEach(function() {
                provider.connect();
                ns = Direct.foo.bar;
            });
            
            it("creates TestAction", function() {
                expect(ns.TestAction).toBeDefined();
            });
            
            it("creates TestAction.Foo", function() {
                expect(ns.TestAction.Foo).toBeDefined();
            });
            
            it("creates TestAction.Foo.Bar", function() {
                expect(ns.TestAction.Foo.Bar).toBeDefined();
            });
            
            it("creates TestAction.Foo.Bar.Baz", function() {
                expect(ns.TestAction.Foo.Bar.Baz).toBeDefined();
            });
            
            it("creates TestAction.Foo.Qux", function() {
                expect(ns.TestAction.Foo.Qux).toBeDefined();
            });
        });
        
        describe("handles nested namespaces the old way:", function() {
            beforeEach(function() {
                provider.disableNestedActions = true;
                provider.connect();
                ns = Direct.foo.bar;
            });
            
            it("creates TestAction", function() {
                expect(ns.TestAction).toBeDefined();
            });
            
            it("creates TestAction.Foo", function() {
                expect(ns['TestAction.Foo']).toBeDefined();
                // AND
                expect(ns.TestAction.Foo).not.toBeDefined();
            });
            
            it("creates TestAction.Foo.Bar", function() {
                expect(ns['TestAction.Foo.Bar']).toBeDefined();
            });
            
            it("creates TestAction.Foo.Bar.Baz", function() {
                expect(ns['TestAction.Foo.Bar.Baz']).toBeDefined();
            });
            
            it("creates TestAction.Foo.Qux", function() {
                expect(ns['TestAction.Foo.Qux']).toBeDefined();
            });
        });
    });
    
    describe("handles remoting methods:", function() {
        var ns;
        
        function checkFn(fn) {
            expect( Ext.isFunction(fn) ).toBeTruthy();
        };
        
        beforeEach(function() {
            provider.connect();
            ns = Direct.foo.bar;
        });
    
        it("has Foo.foo", function() {
            checkFn(ns.TestAction.Foo.foo);
        });
        
        it("has Foo.Bar.bar", function() {
            checkFn(ns.TestAction.Foo.Bar.bar);
        });
        
        it("has Foo.Bar.Baz.baz", function() {
            checkFn(ns.TestAction.Foo.Bar.Baz.baz);
        });
        
        it("has Foo.Qux.qux", function() {
            checkFn(ns.TestAction.Foo.Qux.qux);
        });
    });

    describe("runs remoting methods:", function() {
        var ns, echo, options, handler;
        
        function echoStatus(result, event) {
            this.echo = event.status;
        }
        
        function echoResult(result, event) {
            if (event.status) {
                this.echo = result;
            }
        }
        
        function echoFormResult(request, result) {
            this.echo = result.result;
        }
        
        function echoResultAndOptions(result, event, success, options) {
            if (success) {
                this.echo = result;
                this.options = options;
            }
        }
        
        function returnFalse() {
            return false;
        }
        
        function checkEcho() {
            return Ext.isDefined(this.echo);
        }
        
        function checkHandler() {
            return !!handler.callCount;
        }
        
        function waitForEcho(fn, desc, timeout) {
            fn      = fn   || checkEcho;
            desc    = desc || 'callback never fired';
            timeout = timeout != null ? timeout : 100;
            
            waitsFor(fn, desc, timeout);
        }
        
        function expectEcho(want) {
            runs(function() {
                expect(this.echo).toEqual(want);
            });
        }
        
        beforeEach(function() {
            echo    = undefined;
            options = undefined;
            
            spyOn(Ext.Ajax, 'request').andCallFake(simulateDirectRequest);
            
            provider.connect();
            ns = Direct.foo.bar;
            
            handler = jasmine.createSpy('event handler');
        });
        
        afterEach(function() {
            handler = undefined;
        });
        
        describe("handles call mechanics", function() {
            describe("Ajax headers", function() {
                beforeEach(function() {
                    provider.enableBuffer = false;
                    provider.maxRetries = 0;
                    provider.headers = { blergo: 'frobbe', zingbong: 42 };
                    
                    Ext.Ajax.request.andCallFake(function(request) {
                        return request;
                    });
                });
                
                it("should pass the headers", function() {
                    ns.TestAction.echo('foo', Ext.emptyFn);
                    
                    var args = Ext.Ajax.request.mostRecentCall.args;
                    
                    expect(args[0].headers).toEqual({
                        blergo: 'frobbe',
                        zingbong: 42
                    });
                });
            });
            
            describe("call batching", function() {
                afterEach(function() {
                    // Transactions in this suite have no chance of finishing,
                    // so we clean them up manually
                    if (provider.callTask) {
                        provider.callTask.cancel();
                    }
                    
                    Ext.direct.Manager.transactions.clear();
                });
                
                it("should batch calls within specified enableBuffer timeout", function() {
                    var options, baseTid;
                
                    runs(function() {
                        Ext.Ajax.request.andCallFake(function(opt) {
                            options = opt;
                        });

                        baseTid = Ext.direct.Transaction.TRANSACTION_ID;
                    
                        ns.TestAction.echo('foo', Ext.emptyFn);
                        ns.TestAction.echo('bar', Ext.emptyFn);
                    });
                
                    waitsFor(function() { return !!options }, 'options never modified', 20);
                
                    runs(function() {
                        expect(options.jsonData).toEqual([{
                            action: 'TestAction',
                            method: 'echo',
                            type:   'rpc',
                            tid:    baseTid + 1,
                            data:   ['foo']
                        }, {
                            action: 'TestAction',
                            method: 'echo',
                            type:   'rpc',
                            tid:    baseTid + 2,
                            data:   ['bar']
                        }]);
                    });
                });
                
                it("should run calls with specified timeout w/o batching", function() {
                    var options = [],
                        baseTid;
                
                    runs(function() {
                        Ext.Ajax.request.andCallFake(function(opt) {
                            options.push(opt);
                        });

                        provider.enableBuffer = 200;
                        baseTid = Ext.direct.Transaction.TRANSACTION_ID;
                    
                        ns.TestAction.echo('baz', Ext.emptyFn);
                        ns.TestAction.echo('qux', Ext.emptyFn, this, { timeout: 1 });
                    });
                
                    waitsFor(function() { return !!options }, 'options never modified', 20);
                
                    runs(function() {
                        expect(options.length).toBe(1);
                        // AND
                        expect(options[0].jsonData).toEqual({
                            action: 'TestAction',
                            method: 'echo',
                            type:   'rpc',
                            tid:    baseTid + 2,
                            data:   ['qux']
                        });
                    });
                });
                
                it("should run calls instantly with disableBatching", function() {
                    var options = [],
                        baseTid;
                
                    runs(function() {
                        Ext.Ajax.request.andCallFake(function(opt) {
                            options.push(opt);
                        });

                        provider.enableBuffer = 200;
                        baseTid = Ext.direct.Transaction.TRANSACTION_ID;
                        
                        ns.TestAction.echo('baz', Ext.emptyFn);
                        
                        ns.TestAction.echo.$directCfg.method.disableBatching = true;
                        ns.TestAction.echo('qux', Ext.emptyFn);
                    });
                
                    waitsFor(function() { return !!options }, 'options never modified', 20);
                
                    runs(function() {
                        expect(options.length).toBe(1);
                        // AND
                        expect(options[0].jsonData).toEqual({
                            action: 'TestAction',
                            method: 'echo',
                            type:   'rpc',
                            tid:    baseTid + 2,
                            data:   ['qux']
                        });
                    });
                });
                
                it("should run calls instantly with enableBuffer = false", function() {
                    var option, baseTid;
                    
                    Ext.Ajax.request.andCallFake(function(opt) {
                        options = opt;
                    });
                    
                    provider.enableBuffer = false;
                    baseTid = Ext.direct.Transaction.TRANSACTION_ID;
                    
                    ns.TestAction.echo('fred', Ext.emptyFn);
                    
                    expect(options.jsonData).toEqual({
                        action: 'TestAction',
                        method: 'echo',
                        type: 'rpc',
                        tid: baseTid + 1,
                        data: ['fred']
                    });
                });
                
                describe("bufferLimit", function() {
                    var options, baseTid;
                    
                    beforeEach(function() {
                        options = [];
                        
                        Ext.Ajax.request.andCallFake(function(opt) {
                            options.push(opt);
                        });
                    
                        provider.enableBuffer = 200;
                        provider.bufferLimit = 3;
                    
                        baseTid = Ext.direct.Transaction.TRANSACTION_ID;
                    });
                    
                    it("should batch calls up to bufferLimit", function() {
                        runs(function() {
                            ns.TestAction.echo('fee', Ext.emptyFn);
                            ns.TestAction.echo('fie', Ext.emptyFn);
                            ns.TestAction.echo('foe', Ext.emptyFn);
                            ns.TestAction.echo('foo', Ext.emptyFn);
                        });
                        
                        waitsFor(function() {
                            return !!options.length;
                        }, 'options never modified', 20);
                        
                        runs(function() {
                            expect(options.length).toBe(1);
                            
                            expect(options[0].jsonData).toEqual([{
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 1,
                                data:   ['fee']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 2,
                                data:   ['fie']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 3,
                                data:   ['foe']
                            }]);
                        });
                    });
                    
                    it("should make 2 batched requests for 6 calls", function() {
                        runs(function() {
                            ns.TestAction.echo('frobbe', Ext.emptyFn);
                            ns.TestAction.echo('throbbe', Ext.emptyFn);
                            ns.TestAction.echo('gurgle', Ext.emptyFn);
                            ns.TestAction.echo('bonzo', Ext.emptyFn);
                            ns.TestAction.echo('mymse', Ext.emptyFn);
                            ns.TestAction.echo('splurge', Ext.emptyFn);
                        });
                        
                        waitsFor(function() {
                            return !!options.length;
                        }, 'options never modified', 20);
                        
                        runs(function() {
                            expect(options.length).toBe(2);
                            
                            expect(options[0].jsonData).toEqual([{
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 1,
                                data:   ['frobbe']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 2,
                                data:   ['throbbe']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 3,
                                data:   ['gurgle']
                            }]);
                            
                            expect(options[1].jsonData).toEqual([{
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 4,
                                data:   ['bonzo']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 5,
                                data:   ['mymse']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 6,
                                data:   ['splurge']
                            }]);
                        });
                    });
                    
                    it("should make 3 batched requests for 7 calls", function() {
                        runs(function() {
                            ns.TestAction.echo('Grumpy', Ext.emptyFn);
                            ns.TestAction.echo('Sleepy', Ext.emptyFn);
                            ns.TestAction.echo('Dopey', Ext.emptyFn);
                            ns.TestAction.echo('Bashful', Ext.emptyFn);
                            ns.TestAction.echo('Sneezy', Ext.emptyFn);
                            ns.TestAction.echo('Happy', Ext.emptyFn);
                            ns.TestAction.echo('Doc', Ext.emptyFn);
                        });
                        
                        waitsFor(function() {
                            return options.length === 3;
                        }, '3 Ajax requests', 300);
                        
                        runs(function() {
                            expect(options.length).toBe(3);
                            
                            expect(options[0].jsonData).toEqual([{
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 1,
                                data:   ['Grumpy']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 2,
                                data:   ['Sleepy']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 3,
                                data:   ['Dopey']
                            }]);
                            
                            expect(options[1].jsonData).toEqual([{
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 4,
                                data:   ['Bashful']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 5,
                                data:   ['Sneezy']
                            }, {
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 6,
                                data:   ['Happy']
                            }]);
                            
                            expect(options[2].jsonData).toEqual({
                                action: 'TestAction',
                                method: 'echo',
                                type:   'rpc',
                                tid:    baseTid + 7,
                                data:   ['Doc']
                            });
                        });
                    });
                });
            });
            
            describe("call related events", function() {
                it("fires 'beforecall' event", function() {
                    runs(function() {
                        provider.on('beforecall', handler);
                        
                        ns.TestAction.echo('fred', Ext.emptyFn);
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 20);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                    });

                    waits(35);
                });
                
                it("fires 'call' event", function() {
                    runs(function() {
                        provider.on('call', handler);
                        
                        ns.TestAction.echo('plugh', Ext.emptyFn);
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 20);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                    });
                    
                    waits(35);
                });
                
                it("cancels request when 'beforecall' handler returns false", function() {
                    runs(function() {
                        handler.andCallFake(returnFalse);
                        
                        provider.on('beforecall', handler);
                        
                        ns.TestAction.echo('mymse', Ext.emptyFn);
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 200);
                    
                    // Additional timeout for callbacks to queue and fire
                    waits(20);
                    
                    runs(function() {
                        expect(options).toBeUndefined();
                    });

                    waits(35);
                });
            });
        });
        
        describe("with connection failed", function() {
            it("retries failed transactions", function() {
                var retrySpy = spyOn(Ext.direct.Transaction.prototype, 'retry');
                
                runs(function() {
                    provider.maxRetries = 1;
                    
                    ns.TestAction.echo('foo', Ext.emptyFn, this, { timeout: 666 });
                });
                
                waitsForSpy(retrySpy, 'transaction.retry() never called', 200);
                
                runs(function() {
                    expect(retrySpy).toHaveBeenCalled();
                });
            });
            
            it("fires exception when retry count is exceeded", function() {
                runs(function() {
                    provider.on('data', handler);
            
                    ns.TestAction.echo('bar', Ext.emptyFn, this, { timeout: 666 });
                });
                
                waitsFor(checkHandler, 'event handler never fired', 200);
                
                runs(function() {
                    expect(handler).toHaveBeenCalled();
                });
            });
            
            describe("handles callback:", function() {
                it("fires 'beforecallback' event", function() {
                    runs(function() {
                        provider.on('beforecallback', handler);
                    
                        ns.TestAction.echo('baz', Ext.emptyFn, this, { timeout: 666 });
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 200);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                    });
                });
                
                it("cancels callback when 'beforecallback' handler returns false", function() {
                    var cb = jasmine.createSpy('callback');
                    
                    runs(function() {
                        handler.andCallFake(returnFalse);
                        
                        provider.on('beforecallback', handler);
                        
                        ns.TestAction.echo('qux', cb, this, { timeout: 666 });
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 200);
                    
                    // Additional timeout for callback to be handled
                    waits(20);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                        // AND
                        expect(cb).not.toHaveBeenCalled();
                    });
                });
                
                it("fires callback when retry count is exceeded", function() {
                    runs(function() {
                        ns.TestAction.echo('plugh', echoStatus, this, { timeout: 666 });
                    });
                    
                    waitsFor(checkEcho, 'callback never fired', 200);
                
                    runs(function() {
                        expect(this.echo).toBe(false);
                    });
                });
            });
        });
        
        describe("successfully connected:", function() {
            describe("events", function() {
                describe("data", function() {
                    beforeEach(function() {
                        provider.on('data', handler);
                        
                        // Sanity check, this is applied to every it()
                        expect(handler).not.toHaveBeenCalled();
                    });
                    
                    it("fires for valid responses", function() {
                        runs(function() {
                            ns.TestAction.echo('foo', echoResult, this);
                        });
                        
                        waitForEcho();
                        
                        runs(function() {
                            expect(handler).toHaveBeenCalled();
                        });
                    });
                    
                    it("fires for exceptions", function() {
                        runs(function() {
                            ns.TestAction.echo('foo', echoStatus, this, { timeout: 666 });
                        });
                        
                        waitForEcho();
                        
                        runs(function() {
                            expect(handler).toHaveBeenCalled();
                        });
                    });
                    
                    it("fires for unbound responses", function() {
                        runs(function() {
                            responseText = '{"type":"event","msg":"blerg"}';
                            ns.TestAction.echo('gurgle', Ext.emptyFn);
                        });
                        
                        waitForSpy(handler);
                        
                        runs(function() {
                            expect(handler).toHaveBeenCalled();
                        });
                    });
                });
                
                describe("exception", function() {
                    beforeEach(function() {
                        provider.on('exception', handler);
                        
                        // Sanity check
                        expect(handler).not.toHaveBeenCalled();
                    });
                    
                    it("fires for exceptions", function() {
                        runs(function() {
                            ns.TestAction.echo('throbbe', echoStatus, this, { timeout: 666 });
                        });
                        
                        waitForEcho();
                        
                        runs(function() {
                            expect(handler).toHaveBeenCalled();
                        });
                    });
                    
                    it("does NOT fire for valid responses", function() {
                        runs(function() {
                            ns.TestAction.echo('blerg', echoResult, this);
                        });
                        
                        waitForEcho();
                        
                        runs(function() {
                            expect(handler).not.toHaveBeenCalled();
                        });
                    });
                    
                    it("does NOT fire for unbound responses", function() {
                        var dataSpy = jasmine.createSpy('data event handler');
                        provider.on('data', dataSpy);
                        
                        runs(function() {
                            responseText = '{"type":"event","msg":"zumbo"}';
                            ns.TestAction.echo('fubaru', Ext.emptyFn);
                        });
                        
                        waitForSpy(dataSpy);
                        
                        runs(function() {
                            expect(handler).not.toHaveBeenCalled();
                        });
                    });
                });
            });
            
            describe("handles callback:", function() {
                it("fires 'beforecallback' event", function() {
                    runs(function() {
                        provider.on('beforecallback', handler);
                    
                        ns.TestAction.echo('foo', echoResult, this);
                    });
                    
                    waitsFor(checkEcho, 'event handler never fired', 100);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                    });
                });
                
                it("does not fire 'beforecallback' event for unbound responses", function() {
                    var dataSpy = jasmine.createSpy('data event handler');
                    provider.on('data', dataSpy);
                    
                    runs(function() {
                        responseText = '{"type":"event","msg":"fumble"}';
                        ns.TestAction.echo('throbbe', Ext.emptyFn);
                    });
                    
                    waitForSpy(dataSpy);
                    
                    runs(function() {
                        expect(handler).not.toHaveBeenCalled();
                    });
                });
                
                it("cancels callback when 'beforecallback' handler returns false", function() {
                    var cb = jasmine.createSpy('callback');
                    
                    runs(function() {
                        handler.andCallFake(returnFalse);
                        
                        provider.on('beforecallback', handler);
                        
                        ns.TestAction.echo('bar', cb, this);
                    });
                    
                    waitsFor(checkHandler, 'event handler never fired', 100);
                    
                    // Additional timeout for callback to be handled
                    waits(20);
                    
                    runs(function() {
                        expect(handler).toHaveBeenCalled();
                        // AND
                        expect(cb).not.toHaveBeenCalled();
                    });
                });
                
                it('runs w/o additional options', function() {
                    runs(function() {
                        ns.TestAction.echo('foo', echoResult, this);
                    });
                    
                    waitForEcho();
                    
                    expectEcho('foo');
                });
        
                it('runs w/ additional options', function() {
                    runs(function() {
                        ns.TestAction.echo('bar', echoResultAndOptions, this, {
                            victory: 'Huzzah!'
                        });
                    });
                    
                    waitForEcho();
                    
                    runs(function() {
                        expect(this.echo).toEqual('bar');
                        expect(this.options).toBeDefined();
                        expect(this.options.victory).toEqual('Huzzah!');
                    });
                });
        
                it('runs in nested namespaces', function() {
                    runs(function() {
                        ns.TestAction.Foo.foo(echoResult, this);
                    });
                    
                    waitForEcho();
                    
                    expectEcho('foo');
                });
        
                it('runs in deeply nested namespaces', function() {
                    runs(function() {
                        ns.TestAction.Foo.Bar.bar(echoResult, this);
                    });
                    
                    waitForEcho();
                    
                    expectEcho('bar');
                });
        
                it('runs in really truly deeply nested namespaces', function() {
                    runs(function() {
                        ns.TestAction.Foo.Bar.Baz.baz(echoResult, this);
                    });
                    
                    waitForEcho();
                    
                    expectEcho('baz');
                });
                
                it("does not run for unbound responses", function() {
                    var dataSpy = jasmine.createSpy('data event handler');
                    provider.on('data', dataSpy);
                    
                    runs(function() {
                        responseText = '{"type":"event","msg":"blerg"}';
                        ns.TestAction.echo('frobbe', handler);
                    });
                    
                    waitForSpy(dataSpy);
                    
                    runs(function() {
                        expect(handler).not.toHaveBeenCalled();
                    });
                });
                
                describe("with invalid server responses", function() {
                    var cb;
                    
                    beforeEach(function() {
                        cb = jasmine.createSpy('direct callback');
                        
                        // Console errors are expected
                        spyOn(Ext, 'raise');
                    });
                    
                    afterEach(function() {
                        cb = null;
                    });
                    
                    it("runs when server returns empty string", function() {
                        runs(function() {
                            responseText = '';
                            ns.TestAction.echo('foo', cb);
                        });
                        
                        waitForSpy(cb);
                        
                        runs(function() {
                            expect(cb).toHaveBeenCalled();
                        });
                    });
                    
                    it("runs when server returns 'null'", function() {
                        runs(function() {
                            responseText = 'null';
                            ns.TestAction.echo('bar', cb);
                        });
                        
                        waitForSpy(cb);
                        
                        runs(function() {
                            expect(cb).toHaveBeenCalled();
                        });
                    });
                    
                    it("runs when server returns garbage", function() {
                        runs(function() {
                            responseText = 'zurg foobaroo';
                            ns.TestAction.echo('qux', cb);
                        });
                        
                        waitForSpy(cb);
                        
                        runs(function() {
                            expect(cb).toHaveBeenCalled();
                        });
                    });
                    
                    it("runs all callbacks for batched methods", function() {
                        var cb2 = jasmine.createSpy('direct callback 2');
                        
                        runs(function() {
                            responseText = 'gurgle frob';
                            ns.TestAction.echo('blerg', cb);
                            ns.TestAction.echo('throbbe', cb2);
                        });
                        
                        waitForSpy(cb);
                        
                        runs(function() {
                            expect(cb).toHaveBeenCalled();
                            expect(cb2).toHaveBeenCalled();
                        });
                    });
                });
            });
            
            describe("metadata", function() {
                it("will pass named metadata", function() {
                    runs(function() {
                        ns.TestAction.directMetaNamed('foo', echoResult, this, {
                            metadata: {
                                bleh: 'blah'
                            }
                        });
                    });
                    
                    waitForEcho();
                    
                    expectEcho({ data: 'foo', metadata: { bleh: 'blah' } });
                });
                
                it("will pass ordered metadata", function() {
                    runs(function() {
                        ns.TestAction.directMetaOrdered('bar', echoResult, this, {
                            metadata: ['blerg', 'blam', 'frob']
                        });
                    });
                    
                    waitForEcho();
                    
                    // Metadata len === 2, so 3rd argument should be cut off
                    expectEcho({ data: 'bar', metadata: ['blerg', 'blam'] });
                });
            })
        });
        
        // Modern specific tests are in Ext.form.Panel
        (Ext.toolkit === 'classic' ? describe : xdescribe)("form calls:", function() {
            var form;
            
            function createForm(config) {
                config = Ext.apply({
                    xtype: 'form',
                    renderTo: document.body,
                    width: 300,
                    height: 200,
                    layout: 'form',
                    
                    api: {
                        submit: 'TestAction.directForm'
                    },
                    
                    items: [{
                        xtype: 'hiddenfield',
                        name: 'hidden_foo',
                        value: 'hide the sacred foo from infoodels!'
                    }, {
                        xtype: 'textfield',
                        name: 'overt_foo',
                        value: 'behold the false, deceitful overt foo'
                    }]
                }, config);
                
                form = Ext.widget(config);
            }
            
            beforeEach(function() {
                createForm();
            });
            
            afterEach(function() {
                if (form) {
                    form.destroy();
                }
            });
            
            describe("submit", function() {
                it("should pass field values to direct fn", function() {
                    runs(function() {
                        form.submit({
                            success: echoFormResult,
                            scope: this
                        });
                    });
                    
                    // Callbacks are a bit slow but 2 sec is enough
                    waitsFor(checkEcho, 'callback that never fired', 2000);
                    
                    runs(function() {
                        expect(this.echo).toEqual({
                            success: true,
                            data: {
                                hidden_foo: 'hide the sacred foo from infoodels!',
                                overt_foo: 'behold the false, deceitful overt foo'
                            }
                        });
                    });
                });
                
                it("should pass extra params to direct fn", function() {
                    runs(function() {
                        form.submit({
                            params: {
                                simple_foo: 'barf!'
                            },
                            success: echoFormResult,
                            scope: this
                        });
                    });
                    
                    waitsFor(checkEcho, 'callback that never fired', 2000);
                    
                    runs(function() {
                        expect(this.echo).toEqual({
                            success: true,
                            data: {
                                hidden_foo: 'hide the sacred foo from infoodels!',
                                overt_foo: 'behold the false, deceitful overt foo',
                                simple_foo: 'barf!'
                            }
                        });
                    });
                });
                
                it("should pass form baseParams to direct fn", function() {
                    runs(function() {
                        form.getForm().baseParams = {
                            MEGA_FOO: 'ALL YOUR FOO ARE BELONG TO US!'
                        };
                        
                        form.submit({
                            success: echoFormResult,
                            scope: this
                        });
                    });
                    
                    waitsFor(checkEcho, 'callback that never fired', 2000);
                    
                    runs(function() {
                        expect(this.echo).toEqual({
                            success: true,
                            data: {
                                hidden_foo: 'hide the sacred foo from infoodels!',
                                overt_foo: 'behold the false, deceitful overt foo',
                                MEGA_FOO: 'ALL YOUR FOO ARE BELONG TO US!'
                            }
                        });
                    });
                });
                
                it("should pass named metadata", function() {
                    runs(function() {
                        form.getForm().api.submit = 'TestAction.directMetaFormNamed';
                        
                        form.getForm().metadata = {
                            foo: 'bargh!'
                        };
                        
                        form.submit({
                            success: echoFormResult,
                            scope: this
                        });
                    });
                    
                    waitsFor(checkEcho, 'callback that never fired', 2000);
                    
                    runs(function() {
                        expect(this.echo).toEqual({
                            success: true,
                            data: {
                                hidden_foo: 'hide the sacred foo from infoodels!',
                                overt_foo: 'behold the false, deceitful overt foo'
                            },
                            
                            // JSONified!
                            metadata: '{"foo":"bargh!"}'
                        });
                    });
                });
                
                it("should pass ordered metadata", function() {
                    runs(function() {
                        form.getForm().api.submit = 'TestAction.directMetaFormOrdered';
                        
                        form.getForm().metadata = ['bram', 'blam', 'qux?'];
                        
                        form.submit({
                            success: echoFormResult,
                            scope: this
                        });
                    });
                    
                    waitsFor(checkEcho, 'callback that never fired', 2000);
                    
                    runs(function() {
                        expect(this.echo).toEqual({
                            success: true,
                            data: {
                                hidden_foo: 'hide the sacred foo from infoodels!',
                                overt_foo: 'behold the false, deceitful overt foo'
                            },
                            
                            // JSONified!
                            metadata: '["bram","blam"]'
                        });
                    });
                });
                
                describe("timeout", function() {
                    var ajaxSpy;
                    
                    beforeEach(function() {
                        provider.timeout = 42;
                        ajaxSpy = Ext.Ajax.request;
                        ajaxSpy.andCallFake(Ext.emptyFn);
                    });
                    
                    afterEach(function() {
                        ajaxSpy = null;
                    });
                    
                    it("should respect provider timeout config", function() {
                        runs(function() {
                            form.submit({});
                        });
                        
                        waitForSpy(ajaxSpy);
                        
                        runs(function() {
                            var args = ajaxSpy.mostRecentCall.args[0];
                            
                            expect(args.timeout).toBe(42);
                        });
                    });
                });
            });
        });
    });
});
