define([
    'jquery',
    'lodash',
    'bitbucket/internal/widget/paged-scrollable'
], function(
    $,
    _,
    PagedScrollable
) {

    'use strict';

    module('widget-paged-scrollable', {
        setup : function() {
            this.$scrollContent = $('<div id="scroll-content" />');
            $('#qunit-fixture').css({
                width : '800px',
                height: '600px'
            }).append(this.$scrollContent);
        },
        teardown : function() {
            this.$scrollContent.remove();
        }
    });

    var pageSize = 500;

    function MockPage(start, size, isLastPage, values) {
        this.start = start;
        this.size = size;
        this.limit = pageSize;
        this.isLastPage = isLastPage;
        this.values = values;
    }

    function createPageRequestAssertions(expectedPage) {
        return function(start, limit) {
            strictEqual(start, expectedPage.start, 'Page request start should match');
            strictEqual(limit, expectedPage.limit, 'page request limit should match');
        };
    }

    function partitionIntoPages(dataset) {
        var pages = [];
        for (var start = 0; start < dataset.length; start += pageSize) {
            var values = dataset.slice(start, start + pageSize);
            pages.push(new MockPage(
                start,
                values.length,
                start + pageSize >= dataset.length,
                values
            ));
        }
        return pages;
    }

    function getPuppetScrollable(attachNewContent, idForEntity) {
        var puppetMaster;
        var scrollable = new PagedScrollable('#qunit-fixture', {
            pageSize : pageSize,
            bufferPixels: 0,
            scrollDelay : 0,
            idForEntity: idForEntity
        });
        var requestDataPromise;
        var requestDataAssertions;
        scrollable.requestData = function(start, limit) {
            if (!requestDataPromise) {
                throw new Error("Unexpected request for data.");
            }

            var handlerPromise = requestDataPromise;
            requestDataPromise = null;

            if (requestDataAssertions) {
                requestDataAssertions(start, limit);
                requestDataAssertions = null;
            }

            return handlerPromise;
        };
        scrollable.attachNewContent = attachNewContent || function() {
            puppetMaster.increaseContentHeight(10 * pageSize);
        };

        var scrollListeners = $.Callbacks();
        scrollable.addScrollListener = function(func) { scrollListeners.add(func); };

        scrollable.getScrollTop = function() { return this.__scrollTop__; };
        scrollable.setScrollTop = function(scrollTop) { this.__scrollTop__ = scrollTop; };
        scrollable.getPaneHeight = function() { return this.__containerHeight__; };
        scrollable.getContentHeight = function() { return this.__contentHeight__; };

        scrollable.__scrollTop__ = 0;
        scrollable.__containerHeight__ = 600;
        scrollable.__contentHeight__ = 100;

        puppetMaster = {
            induceScrollTo: function(y) {
                scrollable.__scrollTop__ = y;
                scrollListeners.fire();
            },
            scrollToBottom : function() {
                this.induceScrollTo(scrollable.getContentHeight() - scrollable.getPaneHeight());
            },
            increaseContentHeight : function(dy) {
                scrollable.__contentHeight__ += dy;
            },
            expectDataRequest: function(name, responseOrPromise, assertions) {
                this.ensureRequestsMade();

                if (responseOrPromise.done && responseOrPromise.fail) {
                    requestDataPromise = responseOrPromise;
                } else {
                    requestDataPromise = $.Deferred().resolve(responseOrPromise);
                }

                requestDataPromise.name = name;
                requestDataAssertions = assertions;
            },
            ensureRequestsMade : function() {
                if (requestDataPromise) {
                    throw new Error("Previously expected request, '" + requestDataPromise.name + "', was not made.");
                }
            },
            scrollable: scrollable
        };

        return puppetMaster;
    }


    test('ctor does not attach events or make requests', function() {
        expect(0);
        var scrollable = {
            requestData : function() { throw new Error('ctor should not make data requests.'); },
            addScrollListener : function() { throw new Error('ctor should not attach handlers.'); }
        };
        PagedScrollable.call(scrollable, '#container');
    });

    test('init makes initial request', function() {
        var requestDataCalled = false;
        var addScrollListenerCalled = false;
        var scrollable = getPuppetScrollable().scrollable;

        scrollable.requestData = function() { requestDataCalled = true; return $.Deferred(); };

        scrollable.init();
        ok(requestDataCalled, 'Should call requestData.');
    });

    test('onFirstDataLoaded attaches events', function() {
        var addScrollListenerCalled = false;
        var puppetMaster = getPuppetScrollable(function attachNewContent () {
            puppetMaster.increaseContentHeight(10 * pageSize);
        });
        var scrollable = puppetMaster.scrollable;

        puppetMaster.expectDataRequest("initial request", new MockPage(0, pageSize, false));
        scrollable.addScrollListener = function() { addScrollListenerCalled = true; };

        scrollable.init();
        ok(addScrollListenerCalled, 'Should call addScrollListener.');
    });

    test('scrolling down', function() {
        expect(3);

        var puppetMaster = getPuppetScrollable(function attachNewContent() {
            puppetMaster.increaseContentHeight(10 * pageSize);
            ok(true, 'attachNewContent was called.');
        });
        var scrollable = puppetMaster.scrollable;

        puppetMaster.expectDataRequest("initial request", new MockPage(0, pageSize, false));
        scrollable.init();

        puppetMaster.expectDataRequest("second request", new MockPage(pageSize, pageSize, false));
        puppetMaster.scrollToBottom();

        puppetMaster.expectDataRequest("third request", new MockPage(2 * pageSize, pageSize, true));
        puppetMaster.scrollToBottom();

        puppetMaster.ensureRequestsMade();

        puppetMaster.scrollToBottom(); // ensure no more requests are made since we reached the end
    });

    test('scrolling up', function() {
        expect(3);

        var puppetMaster = getPuppetScrollable(function attachNewContent() {
            puppetMaster.increaseContentHeight(10 * pageSize);
            ok(true, 'attachNewContent was called.');
        });
        var scrollable = puppetMaster.scrollable;

        // mimic deeplinking - scroll to the bottom on first page load.
        var firstDataLoaded = scrollable.onFirstDataLoaded;
        scrollable.onFirstDataLoaded = function() {
            firstDataLoaded.apply(this, arguments);
            puppetMaster.scrollToBottom();
        };

        puppetMaster.expectDataRequest("initial request", new MockPage(2 * pageSize, pageSize, true));
        scrollable.init({targetedItem: Math.round(2.5 * pageSize)}); // somewhere in the third page

        puppetMaster.expectDataRequest("second request", new MockPage(pageSize, pageSize, false));
        puppetMaster.induceScrollTo(0);

        puppetMaster.expectDataRequest("third request", new MockPage(0, pageSize, false));
        puppetMaster.induceScrollTo(0);

        puppetMaster.ensureRequestsMade();

        puppetMaster.induceScrollTo(0); // ensure no more requests are made since we reached the beginning
    });

    test('cancelled request', function() {

        var puppetMaster = getPuppetScrollable(function attachNewContent() {
            ok(false, 'attachNewContent should NOT be called for failed requests.');
        });
        var scrollable = puppetMaster.scrollable;

        var abortCalled = false;
        var cancelledXhrResponse = $.Deferred();

        cancelledXhrResponse.abort = function() {
            abortCalled = true;
            this.reject();
        };

        puppetMaster.expectDataRequest("initial request", cancelledXhrResponse);
        scrollable.init();
        puppetMaster.ensureRequestsMade();

        puppetMaster.expectDataRequest("reinit request", $.Deferred());
        scrollable.init();
        puppetMaster.ensureRequestsMade();

        ok(abortCalled, 'Abort should have been called on the unfinished request since the scrollable was reinitialized.');
    });

    test('suspend and resume', function() {
        expect(2);

        var puppetMaster = getPuppetScrollable(function attachNewContent() {
            puppetMaster.increaseContentHeight(10 * pageSize);
            ok(true, 'attachNewContent was called.');
        });
        var scrollable = puppetMaster.scrollable;

        puppetMaster.expectDataRequest("initial request", new MockPage(0, pageSize, false));
        scrollable.init();
        puppetMaster.ensureRequestsMade();

        scrollable.suspend();
        //expect no dataRequest
        puppetMaster.scrollToBottom();

        puppetMaster.expectDataRequest("second request", new MockPage(pageSize, pageSize, false));
        scrollable.resume();
        puppetMaster.ensureRequestsMade();
    });

    test('dedupe', function() {
        var puppetMaster = getPuppetScrollable(null, _.identity);
        var scrollable = puppetMaster.scrollable;
        scrollable.reset();

        var data = null;
        strictEqual(scrollable._dedupe(data), data, 'dedupe should handle null');
        data = {};
        strictEqual(scrollable._dedupe(data), data, 'dedupe should handle empty objects');
        data = { values : ['blah', 'blah', 'foo'], someOtherAttribute: true };
        deepEqual(scrollable._dedupe(data), { values: ['blah', 'foo'], someOtherAttribute: true  }, 'dedupe should handle duplicates in the same object');
        data = { values : ['bar', 'blah', 'foo', 'blag'] };
        deepEqual(scrollable._dedupe(data), { values: ['bar', 'blag']  }, 'dedupe should handle duplicates in separate calls');

        scrollable.reset();
        deepEqual(scrollable._dedupe(data), data, 'dedupe should reset dedupe record when reset is called');
    });

    test('remove', function() {
        var values = [];
        var i;
        var l = pageSize * 3;
        for (i = 0; i < l; i++) {
            values.push('value-' + i);
        }
        var pages = partitionIntoPages(values);
        var puppetMaster = getPuppetScrollable(null, _.identity);
        var scrollable = puppetMaster.scrollable;


        puppetMaster.expectDataRequest("initial request", pages[0], createPageRequestAssertions(pages[0]));
        scrollable.init();

        strictEqual(scrollable.remove('fake-value'), false, 'a fake-value should not affect the paged-scrollable');

        puppetMaster.expectDataRequest("second request", pages[1], createPageRequestAssertions(pages[1]));
        puppetMaster.scrollToBottom();

        var valueToDelete = values.shift();

        strictEqual(scrollable.remove(valueToDelete), true, valueToDelete + 'should have been successfully deleted');
        pages[2].start--; // the next page start should now be offset by one

        puppetMaster.expectDataRequest("third request", pages[2], createPageRequestAssertions(pages[2]));
        puppetMaster.scrollToBottom();

        puppetMaster.ensureRequestsMade();

        puppetMaster.scrollToBottom(); // ensure no more requests are made since we reached the end
    });

});
