import $ from 'jquery';
import _ from 'lodash';
import testHelper from 'test-helper';

import CloneSelector from 'bitbucket/internal/bbui/mirror-clone-selector/mirror-clone-selector';

// FIXME we don't currently process Soy in JS Unit tests so we have to repro a lot of the the Soy template here to make
// these tests work.
testHelper.setTemplates({
    'bitbucket.internal.component.mirroringCloneSelector.main': ({ primary }) => `
        <form class="aui mirroring-clone-selector">
            <button id="available-mirrors-trigger" href="#available-mirrors" disabled>${primary.mirrorName}</button>
            <div id="available-mirrors">
                <ul id="available-mirror-list">
                    ${bitbucket.internal.component.mirroringCloneSelector.mirrorItem({ item: primary, extraClasses: 'primary' })}
                </ul>
        </div>
        <div id="update-push-url-container" class="hidden">
            <input id="update-push-url-input" type="text" class="text quick-copy-text" readonly="readonly" spellcheck="false" />
        </div>
    </form>`,
    'bitbucket.internal.component.mirroringCloneSelector.mirrorItem': ({ item, extraClasses }) => `
        <li class="mirror-item ${extraClasses}" data-id="${item.id ? item.id : ''}"><a href="#">${item.mirrorName}</a></li>
    `,
});

class CloneDialogPageObject {
    constructor($el) {
        this.$el = $el;
    }

    getMirrors() {
        return _.map(this.$el.find('.mirror-item'), item => {
            const $item = $(item);
            return {
                id: $item.data('id'),
                name: $item.text(),
            };
        });
    }

    getMirrorNames() {
        return _.map(this.getMirrors(), 'name');
    }

    getSelectedMirrorName() {
        return this._trigger().text();
    }

    isPushUrlDisabled() {
        return this._find('#update-push-url-container').is('.hidden');
    }

    getPushUrl() {
        if (this.isPushUrlDisabled()) {
            return '';
        }
        return this._find('#update-push-url-input').val();
    }

    selectPrimary() {
        this._find('.mirror-item.primary').click();

        return this;
    }

    selectMirror(name) {
        this._find('.mirror-item')
            .filter((index, el) => $(el).text() === name)
            .click();

        return this;
    }

    isSelectorDisabled() {
        return this._trigger().is(':disabled');
    }

    onTrigger(event, callback) {
        this._trigger().on(event, callback);
        return this;
    }

    isHidden() {
        return this.$el.is(":hidden");
    }

    _trigger() {
        return this._find('#available-mirrors-trigger');
    }

    _find(selector) {
        return this.$el.find(selector);
    }
}

QUnit.module('bitbucket-mirroring-clone-selector', {
    beforeEach: function() {
        this.$el = $('#qunit-fixture');
        this.$el.html('<div id="clone-selector"> </div>');
        this.cloneUrl = '';
        this.cloneProtocol = '';

        this.preferredMirrorId = 'preferred';
        this.mirrorsForRepoDeferred = $.Deferred();

        this.xhr = sinon.useFakeXMLHttpRequest();
        this.requests = [];
        this.xhr.onCreate = req => this.requests.push(req);

        const $selector = this.$el.find('#clone-selector');
        this.view = new CloneSelector($selector, {
            mirrors: [],
            primary: {
                mirrorName: 'Primary',
                links: {
                    clone: [{
                        name: 'http',
                        href: 'primary-url',
                    }, {
                        name: 'ssh',
                        href: 'ssh-primary-url',
                    }],
                },
            },
            preferredMirrorId: this.preferredMirrorId,
            updatePreferredMirror: mirrorId => {
                this.preferredMirrorId = mirrorId;
            },
            updateCloneUrl: (protocol, url) => {
                this.cloneUrl = url;
                this.cloneProtocol = protocol;
            },
            getMirrors: () => this.mirrorsForRepoDeferred.promise(),
            cloneProtocol: 'http',
        });
        this.cloneDriver = new CloneDialogPageObject($selector);
    },
    afterEach: function() {
        this.view.destroy();
        this.xhr.restore();
    },
});

QUnit.test('Add mirrors', function(assert) {
    assert.ok(this.cloneDriver.isSelectorDisabled(), 'Selector is disabled');
    assert.equal(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    this.view.addMirror({
        available: false,
        mirrorName: 'Unvailableland',
        links: {
            clone: [],
        },
    });

    assert.ok(this.cloneDriver.isSelectorDisabled(), 'Selector is disabled');
    assert.equal(this.cloneDriver.getSelectedMirrorName(), 'Primary', 'Primary is selected.');
    assert.deepEqual(this.cloneDriver.getMirrors(), [{ name: 'Primary', id: "" }], 'Primary only allowed component.');

    this.view.addMirror({
        id: 12,
        available: true,
        mirrorName: 'Asgard',
        links: {
            clone: [],
        },
    });

    assert.ok(!this.cloneDriver.isSelectorDisabled(), 'Selector is enabled now there is a mirror');
    assert.equal(this.cloneDriver.getSelectedMirrorName(), 'Primary', 'Primary is selected.');
    assert.deepEqual(this.cloneDriver.getMirrors(),
        [{ name: 'Primary', id: "" }, { name: 'Asgard', id: 12 }], 'Added mirror to available list.');

    this.view.addMirror({
        id: 13,
        available: true,
        mirrorName: 'ToyTown',
        links: {
            clone: [],
        },
    });

    assert.ok(!this.cloneDriver.isSelectorDisabled(), 'Selector still enabled');
    assert.equal(this.cloneDriver.getSelectedMirrorName(), 'Primary', 'Primary is selected.');
    assert.deepEqual(this.cloneDriver.getMirrors(),
        [{ name: 'Primary', id: "" }, { name: 'Asgard', id: 12 }, { name: 'ToyTown', id: 13 }], 'Added mirror to available list.');
});

QUnit.test('Update clone urls', function(assert) {
    this.view.addMirror({
        available: true,
        id: 'high-hopes',
        mirrorName: 'Tetradome',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    });

    assert.strictEqual(this.cloneUrl, '', 'Clone URL input empty');

    this.cloneDriver.selectPrimary();
    assert.strictEqual(this.cloneUrl, 'primary-url', 'Clone URL input showing primary link #1');

    this.cloneDriver.selectMirror('Tetradome');
    assert.strictEqual(this.cloneUrl, 'mirror-url', 'Clone URL input showing mirror link');

    this.cloneDriver.selectPrimary();
    assert.strictEqual(this.cloneUrl, 'primary-url', 'Clone URL input showing primary link #1');
});

QUnit.test('Set protocol updates the URLs', function(assert) {

    this.view.addMirror({
        available: true,
        id: 'high-hopes',
        mirrorName: 'Tetradome',
        links: {
            clone: [{
                name: 'http',
                href: 'http-mirror-url',
            }, {
                name: 'ssh',
                href: 'ssh-mirror-url',
            }],
        },
    });

    assert.strictEqual(this.cloneUrl, '', 'Clone URL is empty.');
    assert.strictEqual(this.cloneProtocol, '', 'Clone protocol is empty.');

    this.view.updateCloneProtocol('ssh');
    assert.strictEqual(this.view.getProtocol(), 'ssh', 'Protocol updated on view.');
    assert.strictEqual(this.cloneProtocol, 'ssh', 'Clone Protocol updated.');
    assert.strictEqual(this.cloneUrl, 'ssh-primary-url', 'Clone Protocol updated.');

    this.view.updateCloneProtocol('http');
    assert.strictEqual(this.view.getProtocol(), 'http', 'Protocol updated on view.');
    assert.strictEqual(this.cloneProtocol, 'http', 'Clone Protocol updated.');
    assert.strictEqual(this.cloneUrl, 'primary-url', 'Clone Protocol updated.');
});

QUnit.test('Update push url', function(assert) {
    this.view.addMirror({
        available: true,
        id: 'high-hopes',
        mirrorName: 'Tetradome',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }, {
                name: 'ssh',
                href: 'ssh-mirror-url',
            }],
        },
    });

    assert.strictEqual(this.cloneDriver.getPushUrl(), '', 'Push URL not displayed for primary');
    assert.ok(this.cloneDriver.isPushUrlDisabled(), 'Push URL not displayed for primary');

    this.cloneDriver.selectMirror('Tetradome');
    assert.ok(!this.cloneDriver.isPushUrlDisabled(), 'Push URL displayed for mirror');
    assert.strictEqual(this.cloneDriver.getPushUrl(), 'git remote set-url --push origin primary-url');

    this.view.updateCloneProtocol('ssh');
    assert.ok(!this.cloneDriver.isPushUrlDisabled(), 'Push URL displayed for mirror');
    assert.strictEqual(this.cloneDriver.getPushUrl(), 'git remote set-url --push origin ssh-primary-url');

    this.cloneDriver.selectPrimary();
    assert.ok(this.cloneDriver.isPushUrlDisabled(), 'Push URL not displayed for primary');
    assert.strictEqual(this.cloneDriver.getPushUrl(), '', 'Push URL not displayed for primary');
});

QUnit.test('Hide dropdown hides dropdown', function(assert) {
    const calledInvoke = sinon.spy();
    this.cloneDriver.onTrigger('aui-button-invoke', calledInvoke);
    this.view.addMirror({
        available: true,
        id: 'high-hopes',
        mirrorName: 'Tetradome',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    });

    assert.strictEqual(calledInvoke.callCount, 0, 'Trigger button not invoked');
    this.view.hideDropdown();
    assert.strictEqual(calledInvoke.callCount, 1, 'Trigger invoked. Dropdown hidden.');
});

QUnit.test('Update select mirror', function(assert) {
    this.view.addMirror({
        available: true,
        id: 'high-hopes',
        mirrorName: 'Tetradome',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    });

    this.cloneDriver.selectMirror('Tetradome');
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Tetradome');
    assert.strictEqual(this.preferredMirrorId, 'high-hopes');

    this.cloneDriver.selectPrimary();
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');
    assert.strictEqual(this.preferredMirrorId, undefined);
});

QUnit.test('Load mirrors queries all the mirrors', function(assert) {

    //Primary is always rendered.
    const expectedMirrors = [{ name: 'Primary', id: "" }];
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);

    this.view.load();

    //Nothing is updated until the state of the mirrors is known.
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);

    //Resolve the initial call to the upstream.
    this.mirrorsForRepoDeferred.resolve([{
        id: 'mirror',
        name: 'zMirror',
        url: 'http://example.com/mirror',
    }, {
        id: 'mirror2',
        name: 'aMirror2',
        url: 'http://example.com/mirror2',
    }, {
        id: 'bad-mirror',
        name: 'Bad Mirror',
        url: 'http://example.com/badmirror',
    }, {
        id: 'not-available',
        name: 'Not Available',
        url: 'http://example.com/notavailable',
    }]);

    //This will trigger one load per server.
    assert.strictEqual(this.requests.length, 4, "One request made for each mirror.");
    assert.strictEqual(this.requests[0].url, 'http://example.com/mirror');
    assert.strictEqual(this.requests[1].url, 'http://example.com/mirror2');
    assert.strictEqual(this.requests[2].url, 'http://example.com/badmirror');
    assert.strictEqual(this.requests[3].url, 'http://example.com/notavailable');

    //The UI will not change until the responses come back.
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);

    this.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'zMirror',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    //The UI should be updated with the new response.
    expectedMirrors.push({ name: 'zMirror', id: 'mirror' });
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    //The second mirror should be inserted in ABC order.
    this.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'aMirror',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    expectedMirrors.splice(1, 0, { name: 'aMirror', id: 'mirror2' });
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    //Bad responses are ignored.
    this.requests[2].respond(404, { 'Content-Type': 'application/html' }, "FourOhFour");
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    //Unavailable mirrors should be ignored.
    this.requests[3].respond(200, { 'Content-Type': 'application/html' }, JSON.stringify({
        available: false,
        id: 'ignored',
        mirrorName: 'NotAvailable',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');
});

QUnit.test('Preferred mirror loads first & is selected', function(assert) {
    this.view.load();

    const expectedMirrors = [{ name: 'Primary', id: "" }];
    const preferredMirrorId = this.preferredMirrorId;

    //Resolve the initial call to the upstream.
    this.mirrorsForRepoDeferred.resolve([{
        id: 'mirror',
        name: 'zMirror',
        url: 'http://example.com/mirror',
    }, {
        id: this.preferredMirrorId,
        name: 'Preferred',
        url: 'http://example.com/preferred',
    }]);

    //This will trigger one load per server.
    assert.strictEqual(this.requests.length, 2, "One request made for each mirror.");
    assert.strictEqual(this.requests[0].url, 'http://example.com/preferred');
    assert.strictEqual(this.requests[1].url, 'http://example.com/mirror');

    //The UI will not change until the responses come back.
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);

    this.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'zMirror',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    expectedMirrors.push({ name: 'zMirror', id: 'mirror' });
    //The UI should be updated with the new response.
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    //The preferred mirror is now returned. It should be selected by default.
    this.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'Preferred',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    expectedMirrors.splice(1, 0, { name: 'Preferred', id: preferredMirrorId });
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Preferred');
});

QUnit.test('Preferred mirror loads first & but is not selected when user selects new preferred', function(assert) {
    this.view.load();

    const expectedMirrors = [{ name: 'Primary', id: "" }];
    const preferredMirrorId = this.preferredMirrorId;

    //Resolve the initial call to the upstream.
    this.mirrorsForRepoDeferred.resolve([{
        id: 'mirror',
        name: 'zMirror',
        url: 'http://example.com/mirror',
    }, {
        id: this.preferredMirrorId,
        name: 'Preferred',
        url: 'http://example.com/preferred',
    }]);

    //This will trigger one load per server.
    assert.strictEqual(this.requests.length, 2, "One request made for each mirror.");
    assert.strictEqual(this.requests[0].url, 'http://example.com/preferred');
    assert.strictEqual(this.requests[1].url, 'http://example.com/mirror');

    //The UI will not change until the responses come back.
    assert.deepEqual(this.cloneDriver.getMirrorNames(), ['Primary']);

    this.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'zMirror',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    //The UI should be updated with the new response.
    expectedMirrors.push({ name: 'zMirror', id: 'mirror' });
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'Primary');

    //User selects a new preferred mirror before current preferred returns.
    this.cloneDriver.selectMirror('zMirror');
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'zMirror');

    //The preferred mirror is now returned. However the user has selected another preferred mirror
    //so don't change the selection.
    this.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({
        available: true,
        id: 'ignored',
        mirrorName: 'Preferred',
        links: {
            clone: [{
                name: 'http',
                href: 'mirror-url',
            }],
        },
    }));

    expectedMirrors.splice(1, 0, { name: 'Preferred', id: preferredMirrorId });
    assert.deepEqual(this.cloneDriver.getMirrors(), expectedMirrors);
    assert.strictEqual(this.cloneDriver.getSelectedMirrorName(), 'zMirror');
});

QUnit.test('Hide & Show hides the selector', function(assert) {
    assert.ok(!this.cloneDriver.isHidden(), 'Clone dialog not initially hidden');

    this.view.hide();
    assert.ok(this.cloneDriver.isHidden(), 'Clone dialog is hidden on command.');

    this.view.show();
    assert.ok(!this.cloneDriver.isHidden(), 'Clone dialog is shown on command.');
});

