import $ from 'jquery';
import _ from 'lodash';
import PagedScrollable from 'bitbucket/internal/bbui/paged-scrollable/paged-scrollable';
import Widget from 'bitbucket/internal/bbui/widget/widget';

QUnit.module('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();
    },
});

const 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, assert) {
    return function(start, limit) {
        assert.strictEqual(start, expectedPage.start, 'Page request start should match');
        assert.strictEqual(limit, expectedPage.limit, 'page request limit should match');
    };
}

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

function DummyDataProvider() {
    this.reachedEnd = false;
    this.fetchNext = function() {
        return $.Deferred();
    };
    this.abort = sinon.spy(function() {
        this.isFetching = false;
    });

    this.reset = sinon.spy(function() {
        this.initialData = this.currentData = null;
    });
}
DummyDataProvider.prototype = Object.create(Widget.prototype);
DummyDataProvider.prototype.constructor = DummyDataProvider;

function getPuppetScrollable(attachNewContent, idForEntity) {
    const scrollable = new PagedScrollable({
        scrollPaneSelector: '#qunit-fixture',
        pageSize: pageSize,
        bufferPixels: 0,
        scrollDelay: 0,
        idForEntity: idForEntity,
        dataProvider: new DummyDataProvider(),
    });

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

    const puppetMaster = {
        setupDummyFetcher: function() {
            scrollable.provider.fetchNext = function() {
                if (!this.requestDataPromise) {
                    throw new Error("Unexpected request for data.");
                }

                const handlerPromise = this.requestDataPromise;
                this.isFetching = true;
                handlerPromise.done(data => {
                    this.data = data;
                    this.isFetching = false;
                });
                this.requestDataPromise = null;

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

                return handlerPromise;
            };
        },
        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) {
                scrollable.provider.requestDataPromise = responseOrPromise;
            } else {
                scrollable.provider.requestDataPromise = $.Deferred().resolve(responseOrPromise);
            }

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

    sinon.spy(scrollable.provider, 'fetchNext');

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

    scrollable.clear = sinon.spy();

    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;

    return puppetMaster;
}

QUnit.test('ctor does not attach DOM events or make requests', function(assert) {
    const dummyDataProvider = new DummyDataProvider();
    class Scrollable extends PagedScrollable {}
    //const scrollable =  Object.create(PagedScrollable.prototype);
    dummyDataProvider.fetchNext = sinon.spy();
    // set up the spy here because when a new PagedScrollable is created
    // the Widget wraps all methods through bindAll
    const scrollListenerSpy = sinon.spy();
    Scrollable.prototype.addScrollListener = function() {
        scrollListenerSpy();
    };
    const scrollable = new Scrollable({
        scrollPaneSelector: '#container',
        dataProvider: dummyDataProvider,
    });
    assert.equal(scrollable.provider.fetchNext.callCount, 0, 'ctor should not request data');
    assert.equal(scrollListenerSpy.callCount, 0, 'ctor should not attach handlers.');
});

QUnit.test('init makes initial request', function(assert) {
    const scrollable = getPuppetScrollable().scrollable;
    scrollable.init();
    assert.equal(scrollable.clear.callCount, 1, 'Should clear existing items.');
    assert.equal(scrollable.provider.fetchNext.callCount, 1, 'Should request data.');
});

QUnit.test('onFirstDataLoaded attaches events', function(assert) {
    const puppetMaster = getPuppetScrollable(function attachNewContent() {
        puppetMaster.increaseContentHeight(10 * pageSize);
    });
    const scrollable = puppetMaster.scrollable;
    puppetMaster.setupDummyFetcher();
    puppetMaster.expectDataRequest("initial request", new MockPage(0, pageSize, false));
    scrollable.addScrollListener = sinon.spy();

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

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

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

    puppetMaster.setupDummyFetcher();

    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();

    scrollable.provider.reachedEnd = true;
    puppetMaster.scrollToBottom(); // ensure no more requests are made since we reached the end
});

QUnit.test('cancelled request', function(assert) {

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

    const cancelledXhrResponse = $.Deferred();

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

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

    assert.strictEqual(scrollable.provider.abort.callCount, 1, 'Abort should have been called since the scrollable was reinitialized.');
});

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

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

    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();
});

QUnit.test('dedupe arrays', function(assert) {
    const puppetMaster = getPuppetScrollable(null, _.identity);
    const scrollable = puppetMaster.scrollable;
    scrollable.reset();
    puppetMaster.setupDummyFetcher();

    let data = null;
    assert.strictEqual(scrollable._dedupe(data), data, 'dedupe should handle null');
    data = [];
    assert.strictEqual(scrollable._dedupe(data), data, 'dedupe should handle empty arrays');
    data = ['blah', 'blah', 'foo'];
    assert.deepEqual(scrollable._dedupe(data), ['blah', 'foo'], 'dedupe should handle duplicates in the same object');
    data = ['bar', 'blah', 'foo', 'blag'];
    assert.deepEqual(scrollable._dedupe(data), ['bar', 'blag'], 'dedupe should handle duplicates in separate calls');

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

QUnit.test('dedupe objects', function(assert) {
    const puppetMaster = getPuppetScrollable(null, _.identity);
    const scrollable = puppetMaster.scrollable;
    scrollable.reset();
    puppetMaster.setupDummyFetcher();

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

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

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

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

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

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

    const valueToDelete = values.shift();

    assert.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], assert));
    puppetMaster.scrollToBottom();

    puppetMaster.ensureRequestsMade();

    scrollable.provider.reachedEnd = true;
    puppetMaster.scrollToBottom(); // ensure no more requests are made since we reached the end
});

QUnit.test('reset order', function(assert) {
    const scrollable = new PagedScrollable({
        scrollPaneSelector: '#qunit-fixture',
        dataProvider: new DummyDataProvider(),
    });
    scrollable.clear = sinon.spy();
    scrollable.reset = sinon.spy();

    // test that the paged scrollable reset does not reset the data-provider
    scrollable.provider.reset = sinon.spy();
    scrollable.reset();
    assert.strictEqual(scrollable.provider.reset.callCount, 0, 'paged-scrollable reset does not cause data provider to reset');

    // check that a provider reset also resets paged-scrollable
    scrollable.reset.reset(); // reset the spy
    scrollable.provider.trigger('reset');
    assert.strictEqual(scrollable.clear.callCount, 1, 'data provider reset causes a paged-scrollable clear');
    assert.strictEqual(scrollable.reset.callCount, 0, 'data provider reset does not cause a paged-scrollable reset');

});
