/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*global define, describe, it, xit, expect, beforeEach, afterEach, waits, waitsFor, runs, $, waitsForDone, spyOn, jasmine, beforeFirst, afterLast, brackets */
/*unittests: Install Extension Dialog*/

define(function (require, exports, module) {
    "use strict";

    var SpecRunnerUtils = require("spec/SpecRunnerUtils"),
        KeyEvent        = require("utils/KeyEvent"),
        NativeApp       = require("utils/NativeApp"),
        FileSystem,
        Strings;

    describe("Install Extension Dialog", function () {
        var testWindow, dialog, fields, goodInstaller, badInstaller, closed,
            url = "http://brackets.io/extensions/myextension.zip";

        this.category = "integration";

        beforeFirst(function () {
            SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                testWindow = w;
                Strings = testWindow.require("strings");
                FileSystem = testWindow.brackets.test.FileSystem;
            });
        });

        afterLast(function () {
            testWindow = null;
            SpecRunnerUtils.closeTestWindow();
        });

        afterEach(function () {
            testWindow.brackets.config.extension_listing_url = brackets.config.extension_listing_url;
            runs(function () {
                if (dialog) {
                    dialog._close();
                } else {
                    closed = true;
                }
            });
            waitsFor(function () { return closed; }, "dialog closing");
            runs(function () {
                fields = null;
                closed = false;
                dialog = null;
            });
        });

        var successfulResult = {
            installationStatus: "INSTALLED"
        };

        function makeInstaller(succeed, deferred) {
            var installer = {
                install: function () {
                    if (!deferred) {
                        deferred = new $.Deferred();
                        if (succeed) {
                            deferred.resolve(successfulResult);
                        } else {
                            deferred.reject();
                        }
                    }
                    return deferred.promise();
                },
                cancel: function () {
                }
            };
            spyOn(installer, "install").andCallThrough();
            spyOn(installer, "cancel");
            dialog._installer = installer;
            return installer;
        }

        describe("with Browse Extensions enabled", function () {
            it("should open the extension list wiki page when the user clicks on the Browse Extensions button", function () {
                // Set the extension_listing_url in the configuration so that the button will
                // show up.
                var extensionListingURL = "https://github.com/adobe/brackets/wiki/Brackets-Extensions";
                testWindow.brackets.config.extension_listing_url = extensionListingURL;

                dialog = new testWindow.brackets.test.InstallExtensionDialog._Dialog();
                dialog.show()
                    .always(function () {
                        closed = true;
                    });

                fields = {
                    $browseExtensionsButton: dialog.$browseExtensionsButton
                };

                var NativeApp = testWindow.brackets.getModule("utils/NativeApp");
                spyOn(NativeApp, "openURLInDefaultBrowser");
                fields.$browseExtensionsButton.click();
                expect(NativeApp.openURLInDefaultBrowser).toHaveBeenCalledWith(extensionListingURL);
            });
        });

        describe("when user-initiated", function () {

            beforeEach(function () {
                closed = false;
                dialog = new testWindow.brackets.test.InstallExtensionDialog._Dialog();
                dialog.show()
                    .always(function () {
                        closed = true;
                    });
                fields = {
                    $dlg: dialog.$dlg,
                    $okButton: dialog.$okButton,
                    $cancelButton: dialog.$cancelButton,
                    $url: dialog.$url,
                    $browseExtensionsButton: dialog.$browseExtensionsButton
                };
            });

            function setUrl() {
                fields.$url
                    .val(url)
                    .trigger("input");
            }

            it("should open the dialog", function () {
                expect(fields.$dlg[0]).toBeTruthy();
            });

            it("should have the install button disabled when dialog is first open due to empty url field", function () {
                expect(fields.$okButton.prop("disabled")).toBe(true);
            });

            it("should not start install if enter hit while url field is empty", function () {
                var installer = makeInstaller(true);
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(installer.install).not.toHaveBeenCalled();
            });

            it("should close the dialog when Cancel button clicked before entering url", function () {
                fields.$cancelButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            // bootstrap-modal handles Esc on keyup
            it("should close the dialog when Esc pressed before entering url", function () {
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should enable the install button when the url field is nonempty", function () {
                setUrl();
                expect(fields.$okButton.prop("disabled")).toBe(false);
            });

            it("should re-disable the install button if the url field becomes nonempty and then empty", function () {
                setUrl();
                fields.$url
                    .val("")
                    .trigger("input");
                expect(fields.$okButton.prop("disabled")).toBe(true);
            });

            it("should close the dialog when Cancel button clicked after entering url", function () {
                setUrl();
                fields.$cancelButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog when Esc pressed after entering url", function () {
                setUrl();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should start the installer with the given URL when Install button clicked", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                expect(installer.install).toHaveBeenCalledWith(url);
            });

            it("should start the installer with the given URL when Enter is pressed", function () {
                var installer = makeInstaller(true);
                setUrl();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(installer.install).toHaveBeenCalledWith(url);
            });

            it("should disable the ok button while installing", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                expect(fields.$okButton.prop("disabled")).toBe(true);
                deferred.resolve(successfulResult);
            });

            it("should hide the url field and 'browse extensions' button while installing", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                expect(fields.$url.is(":visible")).toBe(false);
                expect(fields.$browseExtensionsButton.is(":visible")).toBe(false);
                deferred.resolve(successfulResult);
            });

            it("should do nothing if Enter pressed while installing", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                installer.install.reset();

                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(installer.install).not.toHaveBeenCalled();
                expect(installer.cancel).not.toHaveBeenCalled();

                deferred.resolve(successfulResult);
            });

            it("should cancel installation if cancel clicked while installing", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                expect(installer.cancel).toHaveBeenCalled();
                deferred.resolve(successfulResult);
            });

            it("should cancel installation if Esc pressed while installing", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(installer.cancel).toHaveBeenCalled();
                deferred.resolve(successfulResult);
            });

            it("should disable the cancel button while cancellation is processed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                expect(fields.$cancelButton.prop("disabled")).toBe(true);
                deferred.reject("CANCELED");
            });

            it("should ignore the Esc key while cancellation is processed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(true);
                deferred.reject("CANCELED");
            });

            it("should ignore the Enter key while cancellation is processed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(true);
                deferred.reject("CANCELED");
            });

            it("should re-enable the ok button and hide cancel button after install succeeds", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve(successfulResult);
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
            });

            it("should re-enable the ok button and hide cancel button after install succeeds synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
            });

            it("should re-enable the ok button and hide cancel button after install fails", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.reject();
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
            });

            it("should re-enable the ok button and hide cancel button after install fails synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
            });

            it("should re-enable the ok button and hide cancel button after install finishes canceling", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                deferred.reject("CANCELED");
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
            });

            it("should close the dialog if ok button clicked after install succeeds", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve(successfulResult);
                fields.$okButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if ok button clicked after install succeeds synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                fields.$okButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Enter pressed after install succeeds", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve(successfulResult);
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Enter pressed after install succeeds synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Esc pressed after install succeeds", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve(successfulResult);
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Esc pressed after install succeeds synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if ok button clicked after install fails", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.reject();
                fields.$okButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if ok button clicked after install fails synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                fields.$okButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Enter pressed after install fails", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.reject();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Enter pressed after install fails synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Esc pressed after install fails", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.reject();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Esc pressed after install fails synchronously", function () {
                var installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if ok button clicked after install finishes canceling", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                deferred.reject("CANCELED");
                fields.$okButton.click();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Enter pressed after install finishes canceling", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                deferred.reject("CANCELED");
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should close the dialog if Esc pressed after install finishes canceling", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(true);
                setUrl();
                fields.$okButton.click();
                fields.$cancelButton.click();
                deferred.reject("CANCELED");
                SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            // Cancellation timing

            it("should time out and re-enable close button if cancel doesn't complete quickly", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                    deferred.reject("CANCELED");
                });
            });

            it("should close when clicking Close button after timing out if cancel doesn't complete quickly", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();
                    expect(fields.$dlg.is(":visible")).toBe(false);
                    deferred.reject("CANCELED");
                });
            });

            it("should close when hitting Enter after timing out if cancel doesn't complete quickly", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();
                    SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keydown", fields.$dlg[0]);
                    deferred.reject("CANCELED");
                });
            });

            it("should close when hitting Esc after timing out if cancel doesn't complete quickly", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();
                    SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_ESCAPE, "keyup", fields.$dlg[0]);
                    deferred.reject("CANCELED");
                });
            });

            // Cancelation vs. successful install race conditions
            it("should keep close button enabled and not throw an exception if install succeeds quickly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                    deferred.resolve(successfulResult);
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should keep close button enabled and not throw an exception if install succeeds slowly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    deferred.resolve(successfulResult);
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should stay closed and not throw an exception if install succeeds after cancelation & force close", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();  // force close
                    deferred.resolve(successfulResult);
                    expect(fields.$dlg.is(":visible")).toBe(false);
                });
            });

            // Cancelation vs. failed install race conditions
            it("should keep close button enabled and not throw an exception if install fails quickly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                    deferred.reject();
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should keep close button enabled and not throw an exception if install fails slowly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    deferred.reject();
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should stay closed and not throw an exception if install fails after cancelation & force close", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();  // force close
                    deferred.reject();
                    expect(fields.$dlg.is(":visible")).toBe(false);
                });
            });

            // Cancelation actually suceeding
            it("should keep close button enabled and not throw an exception if install cancelation completes quickly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                    deferred.reject("CANCELED");
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should keep close button enabled and not throw an exception if install cancelation completes slowly after cancelation", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    deferred.reject("CANCELED");
                    expect(fields.$okButton.prop("disabled")).toBe(false);
                });
            });
            it("should stay closed and not throw an exception if install cancelation completes after cancelation & force close", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                runs(function () {
                    setUrl();
                    dialog._cancelTimeout = 50;
                    fields.$okButton.click();
                    fields.$cancelButton.click();
                });
                waits(100);
                runs(function () {
                    fields.$okButton.click();  // force close
                    deferred.reject("CANCELED");
                    expect(fields.$dlg.is(":visible")).toBe(false);
                });
            });

            it("should not display the Browse Extensions button in the default configuration", function () {
                expect(fields.$browseExtensionsButton.length).toBe(0);
            });

            it("should display a warning message if the extension is already installed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve({
                    installationStatus: "ALREADY_INSTALLED"
                });
                expect(fields.$okButton.text()).toEqual(Strings.OVERWRITE);
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.text()).toEqual(Strings.CANCEL);
                expect(fields.$cancelButton.prop("disabled")).toBe(false);
                expect(fields.$browseExtensionsButton.is(":visible")).toBe(false);
                expect(fields.$dlg.find(".message").text()).toBe(Strings.EXTENSION_ALREADY_INSTALLED);
            });

            it("should display a warning message if the same version is already installed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve({
                    installationStatus: "SAME_VERSION"
                });
                expect(fields.$okButton.text()).toEqual(Strings.OVERWRITE);
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.text()).toEqual(Strings.CANCEL);
                expect(fields.$cancelButton.prop("disabled")).toBe(false);
                expect(fields.$browseExtensionsButton.is(":visible")).toBe(false);
                expect(fields.$dlg.find(".message").text()).toBe(Strings.EXTENSION_SAME_VERSION);
            });

            it("should display a warning message if an older version is already installed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                deferred.resolve({
                    installationStatus: "OLDER_VERSION",
                    installedVersion: "1.0.0",
                    metadata: {
                        version: "0.9.0"
                    }
                });
                expect(fields.$okButton.text()).toEqual(Strings.OVERWRITE);
                expect(fields.$okButton.prop("disabled")).toBe(false);
                expect(fields.$cancelButton.text()).toEqual(Strings.CANCEL);
                expect(fields.$cancelButton.prop("disabled")).toBe(false);
                expect(fields.$browseExtensionsButton.is(":visible")).toBe(false);
                expect(fields.$dlg.find(".message").text().indexOf("0.9.0")).not.toEqual(-1);
            });

            it("should delete the downloaded file if the warning message is cancelled", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                var packageFilename = "/path/to/downloaded/package.zip",
                    file = FileSystem.getFileForPath(packageFilename);
                deferred.resolve({
                    installationStatus: "ALREADY_INSTALLED",
                    localPath: packageFilename
                });
                spyOn(file, "unlink");
                fields.$cancelButton.click();
                expect(file.unlink).toHaveBeenCalled();
                expect(fields.$dlg.is(":visible")).toBe(false);
            });

            it("should pass the installation result if the warning message is confirmed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                var packageFilename = "/path/to/downloaded/package.zip",
                    file = FileSystem.getFileForPath(packageFilename);
                deferred.resolve({
                    installationStatus: "ALREADY_INSTALLED",
                    localPath: packageFilename
                });
                spyOn(file, "unlink");
                var dialogDone = false;
                dialog._dialogDeferred.done(function (result) {
                    dialogDone = true;
                    expect(result.installationStatus).toBe("ALREADY_INSTALLED");
                });
                fields.$okButton.click();
                expect(file.unlink).not.toHaveBeenCalled();
                expect(fields.$dlg.is(":visible")).toBe(false);
                expect(dialogDone).toBe(true);
            });

            it("should pass installation result when update is needed", function () {
                var deferred = new $.Deferred(),
                    installer = makeInstaller(null, deferred);
                setUrl();
                fields.$okButton.click();
                var packageFilename = "/path/to/downloaded/package.zip";
                deferred.resolve({
                    installationStatus: "NEEDS_UPDATE",
                    localPath: packageFilename
                });
                expect(fields.$dlg.is(":visible")).toBe(true);
                expect(fields.$dlg.find(".message").text()).toBe(Strings.EXTENSION_UPDATE_INSTALLED);
                expect(fields.$okButton.text()).toBe(Strings.CLOSE);
                expect(fields.$cancelButton.is(":visible")).toBe(false);
                fields.$okButton.click();
                var dialogDone = false;
                dialog._dialogDeferred.done(function (result) {
                    dialogDone = true;
                    expect(result.installationStatus).toBe("NEEDS_UPDATE");
                });
                expect(dialogDone).toBe(true);
            });
        });

        describe("when initiated from Extension Manager", function () {
            var url = "https://my.repository.io/my-extension.zip", installer;
            beforeEach(function () {
                closed = false;
            });

            it("should immediately start installing from the given url, and in the correct UI state", function () {
                var deferred = new $.Deferred();
                dialog = new testWindow.brackets.test.InstallExtensionDialog._Dialog();
                installer = makeInstaller(null, deferred);
                dialog.show(url)
                    .always(function () {
                        closed = true;
                    });
                fields = {
                    $dlg: dialog.$dlg,
                    $okButton: dialog.$okButton,
                    $cancelButton: dialog.$cancelButton,
                    $url: dialog.$url
                };
                expect(installer.install).toHaveBeenCalledWith(url);
                expect(fields.$okButton.prop("disabled")).toBe(true);
                expect(fields.$url.is(":visible")).toBe(false);
                deferred.resolve(successfulResult);
            });
        });
    });
});


// Add 'file-filter-picker' to keep some margin space on the left of the button

// Set up mouse click event listeners for 'Delete' and 'Edit' buttons

$(_picker).on("select", function (event, item, itemIndex) {

keep keep keep keep replace keep keep keep

/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, window */

/**
 * Button that opens a dropdown list when clicked. More akin to a popup menu than a combobox. Compared to a
 * simple <select> element:
 *  - There's no "selected" state
 *  - The button's label is not automatically changed when an item in the list is clicked
 *  - Its width is not the max of all the dropdown items' labels
 *  - The button & dropdown's appearance can be customized far more
 *
 * TODO: merge DropdownEventHandler into this? Are there any other widgets that might want to use it separately?
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var DropdownEventHandler    = require("utils/DropdownEventHandler").DropdownEventHandler,
        PanelManager            = require("view/PanelManager"),
        Menus                   = require("command/Menus"),
        ViewUtils               = require("utils/ViewUtils"),
        _                       = require("thirdparty/lodash");

    /**
     * Creates a single dropdown-button instance. The DOM node is created but not attached to
     * the document anywhere - clients should append this.$button to the appropriate location.
     *
     * DropdownButton dispatches the following events:
     *  - "select" - when an option in the dropdown is clicked. Passed item object and index.
     *
     * @param {!string} label  Label to display on the button
     * @param {!Array.<*>} items  Items in the dropdown list
     * @param {?function(*, number):!string} itemRenderer  Optional function to convert a single item to HTML
     *          (see itemRenderer() docs below). If not provided, items are assumed to be plain text strings.
     */
    function DropdownButton(label, items, itemRenderer) {
        this.items = items;

        this.itemRenderer = itemRenderer || this.itemRenderer;

        this._onClick        = this._onClick.bind(this);
        this.closeDropdown   = this.closeDropdown.bind(this);
        this._onClickOutside = this._onClickOutside.bind(this);

        this.$button = $("<button class='btn btn-dropdown'/>")
            .text(label)
            .on("click", this._onClick);
    }

    /** @type {!Array.<*>} Items in dropdown list - may be changed any time dropdown isn't open */
    DropdownButton.prototype.items = null;

    /** @type {!jQueryObject} The clickable button. Available as soon as the DropdownButton is constructed. */
    DropdownButton.prototype.$button = null;

    /** @type {?jQueryObject} The dropdown element. Only non-null while open. */
    DropdownButton.prototype.$dropdown = null;

    /** @type {?string} Extra CSS class(es) to apply to $dropdown */
    DropdownButton.prototype.dropdownExtraClasses = null;

    /** @private @type {?HTMLElement} Where to restore focus when dropdown closed */
    DropdownButton.prototype._lastFocus = null;

    /** @private @type {?DropdownEventHandler} Helper object for dropdown. Only non-null while open. */
    DropdownButton.prototype._dropdownEventHandler = null;

    /** @private Handle clicking button */
    DropdownButton.prototype._onClick = function (event) {
        if (!this.$button.hasClass("disabled")) {
            this.toggleDropdown();
        }
        // Indicate click was handled (e.g. to shield from MultiRangeInlineEditor._onClick())
        event.stopPropagation();
    };

    /**
     * Update the button label.
     * @param {string} label
     */
    DropdownButton.prototype.setButtonLabel = function (label) {
        if (!this.$button) {
            return;
        }
        $(this.$button).text(label);
    };

    /**
     * Called for each item when rendering the dropdown.
     * @param {*} item from items array
     * @param {number} index in items array
     * @return {!string} Formatted & escaped HTML
     */
    DropdownButton.prototype.itemRenderer = function (item, index) {
        return _.escape(String(item));
    };

    /** Converts the list of item objects into HTML list items in format required by DropdownEventHandler */
    DropdownButton.prototype._renderList = function () {
        var html = "";
        this.items.forEach(function (item, i) {
            if (item === "---") {
                html += "

";
            } else {
                html += "
";
                html += this.itemRenderer(item, i);
                html += "
";
            }
        }.bind(this));
        return html;
    };

    /**
     * Refresh the dropdown list by removing and re-creating all list items.
     * Call this after deleting/adding any item in the dropdown list.
     */
    DropdownButton.prototype.refresh = function () {
        if (!this.$dropdown) {
            return;
        }

        // Remove all list items and then re-create them from this.items.
        $("li", this.$dropdown).remove();
        this.$dropdown.append(this._renderList());

        // Also trigger openDropdown handler so that custom event handlers can be
        // set up for any custom UI in the list.
        $(this).triggerHandler("openDropdown", [this.$dropdown]);
    };

    /**
     * Check/Uncheck the list item of the given index.
     * @param {number} index The index of the list item to be checked or unchecked
     * @param {boolean} checked True if the list item is to be checked, false to get check
     *    mark removed.
     */
    DropdownButton.prototype.setChecked = function (index, checked) {
        if (!this.$dropdown) {
            return;
        }

        var listItems = $("li", this.$dropdown),
            count     = listItems.length;

        if (index > -1 && index < count) {
            $("a", listItems[index]).toggleClass("checked", checked);
        }
    };

    /** Pops open the dropdown if currently closed. Does nothing if items.length == 0 */
    DropdownButton.prototype.showDropdown = function () {
        // Act like a plain old button if no items to show
        if (!this.items.length) {
            return;
        }

        if (this.$dropdown) {
            return;
        }

        Menus.closeAll();

        var $dropdown = $("
")
            .addClass(this.dropdownExtraClasses)  // (no-op if unspecified)
            .append(this._renderList())
            .appendTo($("body"))
            .data("attached-to", this.$button[0]);  // keep ModalBar open while dropdown focused

        // Calculate position of dropdown
        var toggleOffset   = this.$button.offset(),
            posLeft        = toggleOffset.left,
            posTop         = toggleOffset.top + this.$button.outerHeight(),
            elementRect = {
                top:    posTop,
                left:   posLeft,
                height: $dropdown.height(),
                width:  $dropdown.width()
            },
            clip = ViewUtils.getElementClipSize($(window), elementRect);

        if (clip.bottom > 0) {
            // Bottom is clipped, so move entire menu above button
            posTop = Math.max(0, toggleOffset.top - $dropdown.height() - 4);
        }

        if (clip.right > 0) {
            // Right is clipped, so adjust left to fit menu in editor
            posLeft = Math.max(0, posLeft - clip.right);
        }

        $dropdown.css({
            left: posLeft,
            top: posTop,
            minWidth: this.$button.outerWidth()
        });

        // Attach event handlers
        this._dropdownEventHandler = new DropdownEventHandler($dropdown, this._onSelect.bind(this), this._onDropdownClose.bind(this));
        this._dropdownEventHandler.open();

        window.document.body.addEventListener("mousedown", this._onClickOutside, true);
        $(PanelManager).on("editorAreaResize", this.closeDropdown);

        // Manage focus
        this._lastFocus = window.document.activeElement;
        $dropdown.focus();

        this.$dropdown = $dropdown;

        $(this).triggerHandler("openDropdown", [$dropdown]);
    };

    /**
     * @private
     * Clean up event handlers after dropdown closed & dispose old dropdown DOM. Called regardless of how the dropdown
     * was closed.
     */
    DropdownButton.prototype._onDropdownClose = function () {
        window.document.body.removeEventListener("mousedown", this._onClickOutside, true);
        $(PanelManager).off("editorAreaResize", this.closeDropdown);

        // Restore focus to old pos, unless "select" handler changed it
        if (window.document.activeElement === this.$dropdown[0]) {
            this._lastFocus.focus();
        }

        this._dropdownEventHandler = null;
        this.$dropdown = null;  // already remvoed from DOM automatically by PopUpManager
    };

    /** Closes the dropdown if currently open */
    DropdownButton.prototype.closeDropdown = function () {
        if (this._dropdownEventHandler) {
            this._dropdownEventHandler.close();
        }
    };

    /** @private Clicking outside the dropdown closes it */
    DropdownButton.prototype._onClickOutside = function (event) {
        var $container = $(event.target).closest(".dropdownbutton-popup");

        // If click is outside dropdown list or dropdown button, then close dropdown list
        if (!$(event.target).is(this.$button) &&
                ($container.length === 0 || $container[0] !== this.$dropdown[0])) {
            this.closeDropdown();
            event.stopPropagation();
            event.preventDefault();
        }
    };

    /** Opens the dropdown if closed; closes it if open */
    DropdownButton.prototype.toggleDropdown = function () {
        if (this.$dropdown) {
            this.closeDropdown();
        } else {
            this.showDropdown();
        }
    };

    /**
     * @private
     * Callback from DropdownEventHandler when item in dropdown list is selected (via mouse or keyboard)
     * @param {!jQueryObject} $link  The `a` element selected
     */
    DropdownButton.prototype._onSelect = function ($link) {
        var itemIndex = Number($link.data("index"));
        $(this).triggerHandler("select", [this.items[itemIndex], itemIndex]);
    };

    exports.DropdownButton = DropdownButton;
});


// Load modules

var Lab = require('lab');
var Hoek = require('../lib');

// Declare internals

var internals = {};

// Test shortcuts

var expect = Lab.expect;
var before = Lab.before;
var after = Lab.after;
var describe = Lab.experiment;
var it = Lab.test;

describe('Hoek', function () {

    var nestedObj = {
        v: [7,8,9],
        w: /^something$/igm,
        x: {
            a: [1, 2, 3],
            b: 123456,
            c: new Date(),
            d: /hi/igm,
            e: /hello/
        },
        y: 'y',
        z: new Date()
    };

    var dupsArray = [nestedObj, { z: 'z' }, nestedObj];
    var reducedDupsArray = [nestedObj, { z: 'z' }];

    describe('#clone', function () {

        it('should clone a nested object', function (done) {

            var a = nestedObj;
            var b = Hoek.clone(a);

            expect(a).to.deep.equal(b);
            expect(a.z.getTime()).to.equal(b.z.getTime());
            done();
        });

        it('should clone a null object', function (done) {

            var b = Hoek.clone(null);

            expect(b).to.equal(null);
            done();
        });

        it('should not convert undefined properties to null', function (done) {

            var obj = { something: undefined };
            var b = Hoek.clone(obj);

            expect(typeof b.something).to.equal('undefined');
            done();
        });

        it('should not throw on circular reference', function (done) {

            var a = {};
            a.x = a;

            var test = (function () {

                var b = Hoek.clone(a);
            });

            expect(test).to.not.throw();
            done();
        });

        it('should properly clone circular reference', function (done) {

            var x = {
                'z': new Date()
            };
            x.y = x;

            var b = Hoek.clone(x);
            expect(Object.keys(b.y)).to.deep.equal(Object.keys(x))
            expect(b.z).to.not.equal(x.z);
            expect(b.y).to.not.equal(x.y);
            expect(b.y.z).to.not.equal(x.y.z);
            expect(b.y).to.equal(b);
            expect(b.y.y.y.y).to.equal(b);
            done();
        });

        it('should properly clone deeply nested object', function (done) {

            var a = {
                x: {
                    y: {
                        a: [1, 2, 3],
                        b: 123456,
                        c: new Date(),
                        d: /hi/igm,
                        e: /hello/
                    },
                }
            };

            var b = Hoek.clone(a);

            expect(a).to.deep.equal(b);
            expect(a.x.y.c.getTime()).to.equal(b.x.y.c.getTime());
            done();
        });

        it('should properly clone arrays', function (done) {

            var a = [1,2,3];

            var b = Hoek.clone(a);

            expect(a).to.deep.equal(b);
            done();
        });

        it('should perform actual copy for shallow keys (no pass by reference)', function (done) {

            var x = Hoek.clone(nestedObj);
            var y = Hoek.clone(nestedObj);

            // Date
            expect(x.z).to.not.equal(nestedObj.z);
            expect(x.z).to.not.equal(y.z);

            // Regex
            expect(x.w).to.not.equal(nestedObj.w);
            expect(x.w).to.not.equal(y.w);

            // Array
            expect(x.v).to.not.equal(nestedObj.v);
            expect(x.v).to.not.equal(y.v);

            // Immutable(s)
            x.y = 5;
            expect(x.y).to.not.equal(nestedObj.y);
            expect(x.y).to.not.equal(y.y);

            done();
        });

        it('should perform actual copy for deep keys (no pass by reference)', function (done) {

            var x = Hoek.clone(nestedObj);
            var y = Hoek.clone(nestedObj);

            expect(x.x.c).to.not.equal(nestedObj.x.c);
            expect(x.x.c).to.not.equal(y.x.c);

            expect(x.x.c.getTime()).to.equal(nestedObj.x.c.getTime());
            expect(x.x.c.getTime()).to.equal(y.x.c.getTime());
            done();
        });

        it('copies functions with properties', function (done) {

            var a = {
                x: function () { return 1; },
                y: {}
            };
            a.x.z = 'string in function';
            a.x.v = function () { return 2; };
            a.y.u = a.x;

            var b = Hoek.clone(a);
            expect(b.x()).to.equal(1);
            expect(b.x.v()).to.equal(2);
            expect(b.y.u).to.equal(b.x);
            expect(b.x.z).to.equal('string in function');
            done();
        });

        it('should copy a buffer', function(done){
            var tls = {
                key: new Buffer([1,2,3,4,5]),
                cert: new Buffer([1,2,3,4,5,6,10])
            }

            copiedTls = Hoek.clone(tls);
            expect(Buffer.isBuffer(copiedTls.key)).to.equal(true);
            expect(JSON.stringify(copiedTls.key)).to.equal(JSON.stringify(tls.key))
            expect(Buffer.isBuffer(copiedTls.cert)).to.equal(true);
            expect(JSON.stringify(copiedTls.cert)).to.equal(JSON.stringify(tls.cert))
            done();
        });
    });

    describe('#merge', function () {

        it('does not throw if source is null', function (done) {

            var a = {};
            var b = null;
            var c = null;

            expect(function () {

                c = Hoek.merge(a, b);
            }).to.not.throw();

            expect(c).to.equal(a);
            done();
        });

        it('does not throw if source is undefined', function (done) {

            var a = {};
            var b = undefined;
            var c = null;

            expect(function () {

                c = Hoek.merge(a, b);
            }).to.not.throw();

            expect(c).to.equal(a);
            done();
        });

        it('throws if source is not an object', function (done) {

            expect(function () {

                var a = {};
                var b = 0;

                Hoek.merge(a, b);
            }).to.throw('Invalid source value: must be null, undefined, or an object');
            done();
        });

        it('throws if target is not an object', function (done) {

            expect(function () {

                var a = 0;
                var b = {};

                Hoek.merge(a, b);
            }).to.throw('Invalid target value: must be an object');
            done();
        });

        it('throws if target is not an array and source is', function (done) {

            expect(function () {

                var a = {};
                var b = [1, 2];

                Hoek.merge(a, b);
            }).to.throw('Cannot merge array onto an object');
            done();
        });

        it('returns the same object when merging arrays', function (done) {

            var a = [];
            var b = [1, 2];

            expect(Hoek.merge(a, b)).to.equal(a);
            done();
        });

        it('should combine an empty object with a non-empty object', function (done) {

            var a = {};
            var b = nestedObj;

            var c = Hoek.merge(a, b);
            expect(a).to.deep.equal(b);
            expect(c).to.deep.equal(b);
            done();
        });

        it('should override values in target', function (done) {

            var a = { x: 1, y: 2, z: 3, v: 5, t: 'test', m: 'abc' };
            var b = { x: null, z: 4, v: 0, t: { u: 6 }, m: '123' };

            var c = Hoek.merge(a, b);
            expect(c.x).to.equal(null);
            expect(c.y).to.equal(2);
            expect(c.z).to.equal(4);
            expect(c.v).to.equal(0);
            expect(c.m).to.equal('123');
            expect(c.t).to.deep.equal({ u: 6 });
            done();
        });

        it('should override values in target (flip)', function (done) {

            var a = { x: 1, y: 2, z: 3, v: 5, t: 'test', m: 'abc' };
            var b = { x: null, z: 4, v: 0, t: { u: 6 }, m: '123' };

            var d = Hoek.merge(b, a);
            expect(d.x).to.equal(1);
            expect(d.y).to.equal(2);
            expect(d.z).to.equal(3);
            expect(d.v).to.equal(5);
            expect(d.m).to.equal('abc');
            expect(d.t).to.deep.equal('test');
            done();
        });
    });

    describe('#applyToDefaults', function () {

        var defaults = {
            a: 1,
            b: 2,
            c: {
                d: 3,
                e: [5, 6]
            },
            f: 6,
            g: 'test'
        };

        it('should return null if options is false', function (done) {

            var result = Hoek.applyToDefaults(defaults, false);
            expect(result).to.equal(null);
            done();
        });

        it('should return a copy of defaults if options is true', function (done) {

            var result = Hoek.applyToDefaults(defaults, true);
            expect(result).to.deep.equal(result);
            done();
        });

        it('should apply object to defaults', function (done) {

            var obj = {
                a: null,
                c: {
                    e: [4]
                },
                f: 0,
                g: {
                    h: 5
                }
            };

            var result = Hoek.applyToDefaults(defaults, obj);
            expect(result.c.e).to.deep.equal([4]);
            expect(result.a).to.equal(1);
            expect(result.b).to.equal(2);
            expect(result.f).to.equal(0);
            expect(result.g).to.deep.equal({ h: 5 });
            done();
        });
    });

    describe('#unique', function () {

        it('should ensure uniqueness within array of objects based on subkey', function (done) {

            var a = Hoek.unique(dupsArray, 'x');
            expect(a).to.deep.equal(reducedDupsArray);
            done();
        });

        it('removes duplicated without key', function (done) {

            expect(Hoek.unique([1, 2, 3, 4, 2, 1, 5])).to.deep.equal([1, 2, 3, 4, 5]);
            done();
        });
    });

    describe('#mapToObject', function () {

        it('should return null on null array', function (done) {

            var a = Hoek.mapToObject(null);
            expect(a).to.equal(null);
            done();
        });

        it('should convert basic array to existential object', function (done) {

            var keys = [1, 2, 3, 4];
            var a = Hoek.mapToObject(keys);
            for (var i in keys) {
                expect(a[keys[i]]).to.equal(true);
            }
            done();
        });

        it('should convert array of objects to existential object', function (done) {

            var keys = [{ x: 1 }, { x: 2 }, { x: 3 }];
            var subkey = 'x';
            var a = Hoek.mapToObject(keys, subkey);
            for (var i in keys) {
                expect(a[keys[i][subkey]]).to.equal(true);
            }
            done();
        });
    });

    describe('#intersect', function () {

        it('should return the common objects of two arrays', function (done) {

            var array1 = [1, 2, 3, 4, 4, 5, 5];
            var array2 = [5, 4, 5, 6, 7];
            var common = Hoek.intersect(array1, array2);
            expect(common.length).to.equal(2);
            done();
        });

        it('should return just the first common object of two arrays', function (done) {

            var array1 = [1, 2, 3, 4, 4, 5, 5];
            var array2 = [5, 4, 5, 6, 7];
            var common = Hoek.intersect(array1, array2, true);
            expect(common).to.equal(5);
            done();
        });

        it('should return an empty array if either input is null', function (done) {

            expect(Hoek.intersect([1], null).length).to.equal(0);
            expect(Hoek.intersect(null, [1]).length).to.equal(0);
            done();
        });

        it('should return the common objects of object and array', function (done) {

            var array1 = [1, 2, 3, 4, 4, 5, 5];
            var array2 = [5, 4, 5, 6, 7];
            var common = Hoek.intersect(Hoek.mapToObject(array1), array2);
            expect(common.length).to.equal(2);
            done();
        });
    });

    describe('#matchKeys', function () {

        it('should match the existing object keys', function (done) {

            var obj = {
                a: 1,
                b: 2,
                c: 3,
                d: null
            };

            expect(Hoek.matchKeys(obj, ['b', 'c', 'd', 'e'])).to.deep.equal(['b', 'c', 'd']);
            done();
        });
    });

    describe('#flatten', function () {

        it('should return a flat array', function (done) {

            var result = Hoek.flatten([1, 2, [3, 4, [5, 6], [7], 8], [9], [10, [11, 12]], 13]);
            expect(result.length).to.equal(13);
            expect(result).to.deep.equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
            done();
        });
    });

    describe('#removeKeys', function () {

        var objWithHiddenKeys = {
            location: {
                name: 'San Bruno'
            },
            company: {
                name: '@WalmartLabs'
            }
        };

        it('should delete params with definition\'s hide set to true', function (done) {

            var a = Hoek.removeKeys(objWithHiddenKeys, ['location']);
            expect(objWithHiddenKeys.location).to.not.exist;
            expect(objWithHiddenKeys.company).to.exist;
            done();
        });
    });

    describe('#reach', function () {

        var obj = {
            a: {
                b: {
                    c: {
                        d: 1,
                        e: 2
                    },
                    f: 'hello'
                },
                g: {
                    h: 3
                }
            },
            i: function () { }
        };

        it('returns a valid member', function (done) {

            expect(Hoek.reach(obj, 'a.b.c.d')).to.equal(1);
            done();
        });

        it('returns null on null object', function (done) {

            expect(Hoek.reach(null, 'a.b.c.d')).to.not.exist;
            done();
        });

        it('returns null on missing member', function (done) {

            expect(Hoek.reach(obj, 'a.b.c.d.x')).to.not.exist;
            done();
        });

        it('returns null on invalid member', function (done) {

            expect(Hoek.reach(obj, 'a.b.c.d-.x')).to.not.exist;
            done();
        });

        it('returns function member', function (done) {

            expect(typeof Hoek.reach(obj, 'i')).to.equal('function');
            done();
        });
    });

    describe('#inheritAsync', function () {

        it('should inherit selected methods and wrap in async call', function (done) {

            var proto = {
                a: function () {
                    return 'a!';
                },
                b: function () {
                    return 'b!';
                },
                c: function () {
                    throw new Error('c!');
                }
            };

            var targetFunc = function () { };
            targetFunc.prototype.c = function () {

                return 'oops';
            };

            Hoek.inheritAsync(targetFunc, proto, ['a', 'c']);
            var target = new targetFunc();

            expect(typeof target.a).to.equal('function');
            expect(typeof target.c).to.equal('function');
            expect(target.b).to.not.exist;

            target.a(function (err, result) {

                expect(err).to.not.exist;
                expect(result).to.equal('a!');

                target.c(function (err, result) {

                    expect(result).to.not.exist;
                    expect(err.message).to.equal('c!');
                    done();
                });
            });
        });
    });

    describe('#callStack', function () {

        it('should return the full call stack', function (done) {

            var stack = Hoek.callStack();
            expect(stack[0][0]).to.contain('index.js');
            expect(stack[0][2]).to.equal(30);
            done();
        });
    });

    describe('#displayStack ', function () {

        it('should return the full call stack for display', function (done) {

            var stack = Hoek.displayStack();
            expect(stack[0]).to.contain('test/index.js:');
            done();
        });

        it('should include constructor functions correctly', function (done) {

            var Something = function (next) {

                next();
            };

            var something = new Something(function () {

                var stack = Hoek.displayStack();
                expect(stack[1]).to.contain('new Something');
                done();
            });
        });
    });

    describe('#abort', function () {

        it('should exit process when not in test mode', function (done) {

            var env = process.env.NODE_ENV;
            var write = process.stdout.write;
            var exit = process.exit;

            process.env.NODE_ENV = 'nottatest';
            process.stdout.write = function () { };
            process.exit = function (state) {

                process.exit = exit;
                process.env.NODE_ENV = env;
                process.stdout.write = write;

                expect(state).to.equal(1);
                done();
            };

            Hoek.abort('Boom');
        });

        it('should throw when not in test mode and abortThrow is true', function (done) {

            var env = process.env.NODE_ENV;
            process.env.NODE_ENV = 'nottatest';
            Hoek.abortThrow = true;

            var fn = function () {

                Hoek.abort('my error message');
            };

            expect(fn).to.throw('my error message');
            Hoek.abortThrow = false;
            process.env.NODE_ENV = env;

            done();
        });

        it('should respect hideStack argument', function (done) {

            var env = process.env.NODE_ENV;
            var write = process.stdout.write;
            var exit = process.exit;
            var output = '';

            process.exit = function () { };
            process.env.NODE_ENV = '';
            process.stdout.write = function (message) {

                output = message;
            };

            Hoek.abort('my error message', true);

            process.env.NODE_ENV = env;
            process.stdout.write = write;
            process.exit = exit;

            expect(output).to.equal('ABORT: my error message\n\t\n');

            done();
        });

        it('should default to showing stack', function (done) {

            var env = process.env.NODE_ENV;
            var write = process.stdout.write;
            var exit = process.exit;
            var output = '';

            process.exit = function () { };
            process.env.NODE_ENV = '';
            process.stdout.write = function (message) {

                output = message;
            };

            Hoek.abort('my error message');

            process.env.NODE_ENV = env;
            process.stdout.write = write;
            process.exit = exit;

            expect(output).to.contain('index.js');

            done();
        });
    });

    describe('#assert', function () {

        it('should throw an Error when using assert in a test', function (done) {

            var fn = function () {

                Hoek.assert(false, 'my error message');
            };

            expect(fn).to.throw('my error message');
            done();
        });

        it('should throw an Error when using assert in a test with no message', function (done) {

            var fn = function () {

                Hoek.assert(false);
            };

            expect(fn).to.throw('Unknown error');
            done();
        });

        it('should throw an Error when using assert in a test with multipart message', function (done) {

            var fn = function () {

                Hoek.assert(false, 'This', 'is', 'my message');
            };

            expect(fn).to.throw('This is my message');
            done();
        });

        it('should throw an Error when using assert in a test with object message', function (done) {

            var fn = function () {

                Hoek.assert(false, 'This', 'is', { spinal: 'tap' });
            };

            expect(fn).to.throw('This is {"spinal":"tap"}');
            done();
        });

        it('should throw an Error when using assert in a test with error object message', function (done) {

            var fn = function () {

                Hoek.assert(false, new Error('This is spinal tap'));
            };

            expect(fn).to.throw('This is spinal tap');
            done();
        });
    });

    describe('#loadDirModules', function () {

        it('should load modules from directory', function (done) {

            var target = {};
            Hoek.loadDirModules(__dirname + '/modules', ['test2'], target);
            expect(target.Test1.x).to.equal(1);
            expect(target.Test2).to.not.exist;
            expect(target.Test3.z).to.equal(3);
            done();
        });

        it('should list modules from directory into function', function (done) {

            var target = {};
            Hoek.loadDirModules(__dirname + '/modules', ['test2'], function (path, name, capName) {

                target[name] = capName;
            });

            expect(target.test1).to.equal('Test1');
            expect(target.test2).to.not.exist;
            expect(target.test3).to.equal('Test3');
            done();
        });
    });

    describe('#rename', function () {

        it('should rename object key', function (done) {

            var a = { b: 'c' };
            Hoek.rename(a, 'b', 'x');
            expect(a.b).to.not.exist;
            expect(a.x).to.equal('c');
            done();
        });
    });

    describe('Timer', function () {

        it('should return time elapsed', function (done) {

            var timer = new Hoek.Timer();
            setTimeout(function () {

                expect(timer.elapsed()).to.be.above(9);
                done();
            }, 12);
        });
    });

    describe('#loadPackage', function () {

        it('should', function (done) {

            var pack = Hoek.loadPackage();
            expect(pack.name).to.equal('hoek');
            done();
        });
    });

    describe('#escapeRegex', function () {

        it('should escape all special regular expression characters', function (done) {

            var a = Hoek.escapeRegex('4^f$s.4*5+-_?%=#!:@|~\\/`"(>)[<]d{}s,');
            expect(a).to.equal('4\\^f\\$s\\.4\\*5\\+\\-_\\?%\\=#\\!\\:@\\|~\\\\\\/`"\\(>\\)\\[<\\]d\\{\\}s\\,');
            done();
        });
    });

    describe('#toss', function () {

        it('should call callback with new error', function (done) {

            var callback = function (err) {

                expect(err).to.exist;
                expect(err.message).to.equal('bug');
                done();
            };

            Hoek.toss(true, 'feature', callback);
            Hoek.toss(false, 'bug', callback);
        });

        it('should call callback with new error and no message', function (done) {

            Hoek.toss(false, function (err) {

                expect(err).to.exist;
                expect(err.message).to.equal('');
                done();
            });
        });

        it('should call callback with error condition', function (done) {

            Hoek.toss(new Error('boom'), function (err) {

                expect(err).to.exist;
                expect(err.message).to.equal('boom');
                done();
            });
        });

        it('should call callback with new error using message with error condition', function (done) {

            Hoek.toss(new Error('ka'), 'boom', function (err) {

                expect(err).to.exist;
                expect(err.message).to.equal('boom');
                done();
            });
        });

        it('should call callback with new error using passed error with error condition', function (done) {

            Hoek.toss(new Error('ka'), new Error('boom'), function (err) {

                expect(err).to.exist;
                expect(err.message).to.equal('boom');
                done();
            });
        });
    });

    describe('Base64Url', function () {

        var base64str = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w';
        var str = unescape('%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29*+%2C-./0123456789%3A%3B%3C%3D%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF');

        describe('#base64urlEncode', function () {

            it('should base64 URL-safe a string', function (done) {

                expect(Hoek.base64urlEncode(str)).to.equal(base64str);
                done();
            });
        });

        describe('#base64urlDecode', function () {

            it('should un-base64 URL-safe a string', function (done) {

                expect(Hoek.base64urlDecode(base64str)).to.equal(str);
                done();
            });

            it('should return error on undefined input', function (done) {

                expect(Hoek.base64urlDecode().message).to.exist;
                done();
            });

            it('should return error on invalid input', function (done) {

                expect(Hoek.base64urlDecode('*').message).to.exist;
                done();
            });
        });
    });

    describe('#escapeHeaderAttribute', function () {

        it('should not alter ascii values', function (done) {

            var a = Hoek.escapeHeaderAttribute('My Value');
            expect(a).to.equal('My Value');
            done();
        });

        it('should escape all special HTTP header attribute characters', function (done) {

            var a = Hoek.escapeHeaderAttribute('I said go!!!#"' + String.fromCharCode(92));
            expect(a).to.equal('I said go!!!#\\"\\\\');
            done();
        });

        it('should throw on large unicode characters', function (done) {

            var fn = function () {

                Hoek.escapeHeaderAttribute('this is a test' + String.fromCharCode(500) + String.fromCharCode(300));
            };

            expect(fn).to.throw(Error);
            done();
        });

        it('should throw on CRLF to prevent response splitting', function (done) {

            var fn = function () {

                Hoek.escapeHeaderAttribute('this is a test\r\n');
            };

            expect(fn).to.throw(Error);
            done();
        });
    });

    describe('#escapeHtml', function () {

        it('should escape all special HTML characters', function (done) {

            var a = Hoek.escapeHtml('&<>"\'`');
            expect(a).to.equal('&<>"'`');
            done();
        });

        it('should return empty string on falsy input', function (done) {

            var a = Hoek.escapeHtml('');
            expect(a).to.equal('');
            done();
        });

        it('should return unchanged string on no reserved input', function (done) {

            var a = Hoek.escapeHtml('abc');
            expect(a).to.equal('abc');
            done();
        });
    });

    describe('#printEvent', function () {

        it('outputs event as string', function (done) {

            var event = {
                timestamp: (new Date(2013, 1, 1, 6, 30, 45, 123)).getTime(),
                tags: ['a', 'b', 'c'],
                data: { some: 'data' }
            };

            Hoek.consoleFunc = function (string) {

                Hoek.consoleFunc = console.log;
                expect(string).to.equal('130201/063045.123, a, {"some":"data"}');
                done();
            };

            Hoek.printEvent(event);
        });

        it('outputs JSON error', function (done) {

            var event = {
                timestamp: (new Date(2013, 1, 1, 6, 30, 45, 123)).getTime(),
                tags: ['a', 'b', 'c'],
                data: { some: 'data' }
            };

            event.data.a = event.data;

            Hoek.consoleFunc = function (string) {

                Hoek.consoleFunc = console.log;
                expect(string).to.equal('130201/063045.123, a, JSON Error: Converting circular structure to JSON');
                done();
            };

            Hoek.printEvent(event);
        });
    });

    describe('#nextTick', function () {

        it('calls the provided callback on nextTick', function (done) {

            var a = 0;

            var inc = function (step, next) {

                a += step;
                next();
            };

            var ticked = Hoek.nextTick(inc);

            ticked(5, function () {

                expect(a).to.equal(6);
                done();
            });

            expect(a).to.equal(0);
            inc(1, function () {

                expect(a).to.equal(1);
            });
        });
    });
});


/* Persian (Farsi) Translation for the jQuery UI date picker plugin. */
/* Javad Mowlanezhad -- jmowla@gmail.com */
/* Jalali calendar should supported soon! (Its implemented but I have to test it) */
jQuery(function($) {
	$.datepicker.regional['fa'] = {
		closeText: 'بستن',
		prevText: '<قبلی',
		nextText: 'بعدی>',
		currentText: 'امروز',
		monthNames: [
			'فروردين',
			'ارديبهشت',
			'خرداد',
			'تير',
			'مرداد',
			'شهريور',
			'مهر',
			'آبان',
			'آذر',
			'دی',
			'بهمن',
			'اسفند'
		],
		monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
		dayNames: [
			'يکشنبه',
			'دوشنبه',
			'سه‌شنبه',
			'چهارشنبه',
			'پنجشنبه',
			'جمعه',
			'شنبه'
		],
		dayNamesShort: [
			'ی',
			'د',
			'س',
			'چ',
			'پ',
			'ج',
			'ش'
		],
		dayNamesMin: [
			'ی',
			'د',
			'س',
			'چ',
			'پ',
			'ج',
			'ش'
		],
		weekHeader: 'هف',
		dateFormat: 'yy/mm/dd',
		firstDay: 6,
		isRTL: true,
		showMonthAfterYear: false,
		yearSuffix: ''};
	$.datepicker.setDefaults($.datepicker.regional['fa']);
});


body { background-color: red;}

h1 { color: blue; }