define([
    'jquery',
    'test-helper',
    'bitbucket/internal/model/page-state',
    'bitbucket/internal/util/ajax'
], function(
    $,
    testHelper,
    pageState,
    ajax
) {

    'use strict';

    testHelper.setTemplates({
        'bitbucket.internal.widget.errorContent': "Error content."
    });

    module('util-ajax', {
        setupSinon : function() {
            this.server = sinon.fakeServer.create();
            this.clock = sinon.useFakeTimers();

            this.urls = {
                okJson : "/good/url/json/body",
                okNonJson : "/good/url/bad/body",
                notFoundJson : "/bad/url/json/body",
                notFoundNonJson : "/bad/url/bad/body"
            };

            this.server.respondWith(this.urls.notFoundNonJson, [404, {"Content-Type" : "text/html"},
                "non-json body"]);
            this.server.respondWith(this.urls.okNonJson, [200, {"Content-Type" : "text/html"},
                "non-json body"]);
            this.server.respondWith(this.urls.notFoundJson, [404, {"Content-Type" : "application/json"},
                '{ "body" : "is json" }']);
            this.server.respondWith(this.urls.okJson, [200, {"Content-Type" : "application/json"},
                '{ "body" : "is json" }']);
        },
        teardownSinon : function() {
            this.server.restore();
            this.clock.restore();
        },
        setup : function() {
            this.setupSinon();
            sinon.stub(pageState, "getCurrentUser", function () {
                return null;
            });
        },
        teardown : function(){
            this.teardownSinon();
            pageState.getCurrentUser.restore();
        }
    });


    function errorDialogShown(rejectedAjaxPromise) {
        var dialogShown = false;
        rejectedAjaxPromise.fail(function(xhr, textStatus, errorThrown, data, errorDialog) {
            dialogShown = errorDialog && errorDialog.isShowing();
        });
        return dialogShown;
    }
    function cleanupErrorDialog(rejectedAjaxPromise) {
        rejectedAjaxPromise.fail(function(xhr, textStatus, errorThrown, data, errorDialog) {
            if (errorDialog) {
                errorDialog.remove();
            }
        });
    }


    function assertPromise(promise, ajaxOpts) {
        ok(promise && promise.done && promise.fail && promise.then, "A promise should be returned for " + ajaxOpts.url);
    }

    function assertRejected(promise, ajaxOpts) {
        equal(promise.state(), 'rejected', "Promise should be rejected for " + ajaxOpts.url);
    }

    function assertResolved(promise, ajaxOpts) {
        equal(promise.state(), 'resolved', "Promise should be resolved for " + ajaxOpts.url);
    }

    function assertCorrectRejectedParams(promise, isJsonBody, isRest) {
        var wasCalled = false;
        promise.fail(function(xhr, textStatus, errorThrown, data) {
            wasCalled = true;
            ok(xhr && xhr.responseText != null, "xhr should be first param.");
            ok(typeof textStatus === 'string', "textStatus should be second param.");
            ok(typeof errorThrown === 'string' || errorThrown instanceof Error, "errorThrown should be third param.");
            if (isRest) {
                if (isJsonBody) {
                    ok($.isPlainObject(data), "json data should be fourth param.");
                } else {
                    ok(typeof data === 'string', "Default datatype should be HTML doc when nothing specified.");
                }
            }
        });
        ok(wasCalled, ".fail() callback was called.");
    }

    function assertCorrectResolvedParams(promise, isJsonBody) {
        var wasCalled = false;
        promise.done(function(data) {
            wasCalled = true;
            if (isJsonBody) {
                ok($.isPlainObject(data), "json data should be first param.");
            } else {
                ok(typeof data === 'string', "Default datatype should be HTML doc when nothing specified.");
            }
        });
        ok(wasCalled, ".done() callback was called.");
    }

    var isJson = true;
    var isNotJson = false;
    var isRestEndpoint = true;
    var isNotRestEndpoint = false;
    var resolve = true;
    var reject = false;


    //-- ajax

    function ajaxDotAjaxTest(url, isJson, resolve) {
        var options = { url : url };
        var promise = ajax.ajax(options);

        this.server.respond();

        assertPromise(promise, options);
        if (resolve) {
            assertResolved(promise, options);
            assertCorrectResolvedParams(promise, isJson, isNotRestEndpoint);
        } else {
            assertRejected(promise, options);
            assertCorrectRejectedParams(promise, isJson, isNotRestEndpoint);
            cleanupErrorDialog(promise);
        }
    }

    test('ajax.ajax() 404 with String body is REJECTED', function() {
        ajaxDotAjaxTest.call(this, this.urls.notFoundNonJson, isNotJson, reject);
    });

    test('ajax.ajax() 404 with JSON body is REJECTED', function() {
        ajaxDotAjaxTest.call(this, this.urls.notFoundJson, isJson, reject);
    });

    test('ajax.ajax() 200 with String body is RESOLVED', function() {
        ajaxDotAjaxTest.call(this, this.urls.okNonJson, isNotJson, resolve);
    });

    test('ajax.ajax() 200 with JSON body is RESOLVED', function() {
        ajaxDotAjaxTest.call(this, this.urls.okJson, isJson, resolve);
    });

    //-- rest

    function ajaxDotRestTest(url, isJson, resolve) {
        var options = { url : url };
        var promise = ajax.rest(options);

        this.server.respond();

        assertPromise(promise, options);
        if (resolve) {
            assertResolved(promise, options);
            assertCorrectResolvedParams(promise, isJson, isRestEndpoint);
        } else {
            assertRejected(promise, options);
            assertCorrectRejectedParams(promise, isJson, isRestEndpoint);
            cleanupErrorDialog(promise);
        }
    }

    test('ajax.rest() 404 with String body is REJECTED', function() {
        ajaxDotRestTest.call(this, this.urls.notFoundNonJson, isNotJson, reject);
    });

    test('ajax.rest() 404 with JSON body is REJECTED', function() {
        ajaxDotRestTest.call(this, this.urls.notFoundJson, isJson, reject);
    });

    test('ajax.rest() 200 with String body is REJECTED', function() {
        ajaxDotRestTest.call(this, this.urls.okNonJson, isNotJson, reject);
    });

    test('ajax.rest() 200 with JSON body is RESOLVED', function() {
        ajaxDotRestTest.call(this, this.urls.okJson, isJson, resolve);
    });

    // Error handling

    test('ajax accepts custom handlers', function() {
        var handlerCalled = false;
        var options = {
            url: this.urls.notFoundJson,
            statusCode: {
                "404": function (data, textStatus, xhr) {
                    handlerCalled = true;
                    return undefined;
                }
            }
        };
        var promise = ajax.ajax(options);

        this.server.respond();

        assertPromise(promise, options);
        ok(handlerCalled, "The custom handler should have been called");
        assertRejected(promise, options);

        ok(errorDialogShown(promise), "An ErrorDialog should have been created when our handler doesn't return a promise or false.");
        cleanupErrorDialog(promise);
    });

    test('ajax accepts custom handlers that return promises', function() {
        var handlerCalled = false;
        var customPromise = $.Deferred();
        var options = {
            url: this.urls.notFoundJson,
            statusCode: {
                404: function () {
                    handlerCalled = true;
                    return customPromise;
                }
            }
        };
        var promise = ajax.ajax(options);

        this.server.respond();

        assertPromise(promise, options);
        ok(handlerCalled, "The custom handler should have been called");
        strictEqual(promise.state(), 'pending', "The ajax promise should not be resolved or rejected before the custom promise.");

        customPromise.reject();

        assertRejected(promise, options);

        ok(!errorDialogShown(promise), "An ErrorDialog should not have been created when our handler returns a promise");
        cleanupErrorDialog(promise);
    });

    test('ajax accepts custom handlers that return false', function() {
        var handlerCalled = false;
        var options = {
            url: this.urls.notFoundJson,
            statusCode: {
                "404": function (data, textStatus, xhr) {
                    handlerCalled = true;
                    return false;
                }
            }
        };
        var promise = ajax.ajax(options);

        this.server.respond();

        assertPromise(promise, options);
        ok(handlerCalled, "The custom handler should have been called");
        assertRejected(promise, options);

        ok(!errorDialogShown(promise), "An ErrorDialog should not have been created when our handler returns false");
        cleanupErrorDialog(promise);
    });

    test('ajax accepts custom handlers that return error objects', function() {
        var handlerCalled = false;
        var options = {
            url: this.urls.notFoundJson,
            statusCode: {
                "404": function (data, textStatus, xhr) {
                    handlerCalled = true;
                    return { title: 'Custom error title', message: 'Custom error message' };
                }
            }
        };
        var promise = ajax.ajax(options);

        this.server.respond();

        assertPromise(promise, options);
        ok(handlerCalled, "The custom handler should have been called");
        assertRejected(promise, options);

        ok(errorDialogShown(promise), "An ErrorDialog should have been created when our handler returned an error object.");
        cleanupErrorDialog(promise);
    });

    test('ajax.poll - resolve', function() {

        var tickExpecting = function tickExpecting(ms, callCount, promiseState) {
            ms && this.clock.tick(ms);
            this.server.respond();
            equal(tick.callCount, callCount);
            equal(pollPromise.state(), promiseState);
        }.bind(this);

        var tickResults = [undefined, 1];
        var tick = sinon.stub({tick:function(){}}, 'tick', function() {
            return tickResults.shift();
        });

        var pollPromise = ajax.poll({
            url : this.urls.okJson,
            interval : 250,
            delay : 100,
            tick : tick
        });

        tickExpecting(0, 0, 'pending');
        tickExpecting(101, 1, 'pending');
        tickExpecting(248, 1, 'pending');
        tickExpecting(3, 2, 'resolved');
        pollPromise.done(function(json) {
            equal(json.body, 'is json', 'Correct data returned');
        });
    });

    test('ajax.poll - reject', function() {

        var tickExpecting = function tickExpecting(ms, callCount, promiseState) {
            ms && this.clock.tick(ms);
            this.server.respond();
            equal(tick.callCount, callCount, 'Expect callCount of ' + callCount + ' after ' + ms + 'ms');
            equal(pollPromise.state(), promiseState, 'Expect promise state ' + promiseState + ' after ' + ms + 'ms');
        }.bind(this);

        var tickResults = [undefined, null];
        var tick = sinon.stub({tick:function(){}}, 'tick', function() {
            return tickResults.shift();
        });

        var pollPromise = ajax.poll({
            url : this.urls.okJson,
            interval : 250,
            delay : 100,
            tick : tick
        });

        tickExpecting(0, 0, 'pending');
        tickExpecting(101, 1, 'pending');
        tickExpecting(248, 1, 'pending');
        tickExpecting(3, 2, 'rejected');
        pollPromise.fail(function(xhr, textStatus, error, json) {
            equal(json.body, 'is json', 'Correct data returned');
        });
    });
});
