define([
    'lodash',
    'bitbucket/internal/model/page-state',
    'bitbucket/internal/util/client-storage',
    'bitbucket/internal/util/feature-detect',
    'bitbucket/internal/util/function'
], function(
    _,
    pageState,
    clientStorage,
    featureDetect,
    fn
) {
    var testString = (new Date()).getTime().toString();
    var testItemKey = 'client-storage-test-item';
    var cleanupKey = 'lastCleanup';
    var hasCheckedCleanUpKey = 'hasCheckedCleanUp';
    var oldTime = (new Date( (new Date().getFullYear()) - 1 , 0, 1)).getTime();

    function lsKey(key) {
        if (typeof key === 'undefined') {
            return testItemKey;
        }
        return testItemKey + '-' + key;
    }

    var oldItemKey = lsKey('old');

    function prepareForCleanup() {

        // Force cleanup to be skipped.
        sessionStorage.setItem(hasCheckedCleanUpKey, true);

        var item = {
            timestamp: oldTime
        };

        localStorage.setItem(oldItemKey, JSON.stringify(item));

        // Lets remove the 'lastCleanup' and hasCheckedCleanup key - to make sure the cleanup task runs.
        localStorage.removeItem(cleanupKey);
        sessionStorage.removeItem(hasCheckedCleanUpKey);

    }


    function setup() {
        var self = this;
        self.supportsStorage = true;
        var supportsStorage = function() { return self.supportsStorage; };
        sinon.stub(featureDetect, 'localStorage', supportsStorage);
    }

    function teardown() {
        var item;
        //loop through the localStorage and sessionStorage and remove any keys that start with our test prefix
        for (item in localStorage) {
            if (localStorage.hasOwnProperty(item)) {
                if (item.indexOf(lsKey()) === 0) {
                    localStorage.removeItem(item);
                }
            }
        }
        for (item in sessionStorage) {
            if (sessionStorage.hasOwnProperty(item)) {
                if (item.indexOf(lsKey()) === 0) {
                    sessionStorage.removeItem(item);
                }
            }
        }
        clientStorage._resetDummy();
        featureDetect.localStorage.restore();
    }

    // HACK: Setup and teardown can't be run here because QUnit _sometimes_ flips out with asyncTests and setup.
    // It runs a setup before the previous teardown.
    module('util-client-storage');

    function _testWithFeatureDetect(testType, name, fn) {
        testType(name + ' (localStorage disabled)', function() {
            setup.call(this);
            this.supportsStorage = false;
            clientStorage.setItem(lsKey(), testString);
            clientStorage.setSessionItem(lsKey(), testString);
            fn.apply(this, arguments);
            teardown.call(this);
        });

        testType(name + ' (localStorage enabled)', function() {
            setup.call(this);
            this.supportsStorage = true;
            clientStorage.setItem(lsKey(), testString);
            clientStorage.setSessionItem(lsKey(), testString);
            fn.apply(this, arguments);
            teardown.call(this);
        });
    }
    function _test(name, fn) {
        _testWithFeatureDetect(test, name, fn);
    }
    function _asyncTest(name, fn) {
        _testWithFeatureDetect(asyncTest, name, fn);
    }

    function jsonTestData(string) {
        return { "test" : string };
    }
    function jsonStringTestData(string) {
        return JSON.stringify(jsonTestData(string));
    }

    _test('LocalStorage', function(){

        equal(clientStorage.getItem(lsKey()), testString, 'value from previously set item retrieved');
        // remove an item and test that it is removed
        clientStorage.removeItem(lsKey());
        strictEqual(clientStorage.getItem(lsKey()), null, 'A removed item should no longer be accessible');

    });

    test('Test getting a plain and decompressed value', function(){
        // Test that the decompression works, it doesn't matter if it's from localStorage or the dummy hash.

        localStorage.setItem(lsKey('plain'), jsonStringTestData(testString));
        deepEqual(clientStorage.getItem(lsKey('plain')), jsonTestData(testString),
            'plain string value from previously set item retrieved');
        clientStorage.removeSessionItem(lsKey('plain'));
    });

    test('Test special characters when getting a plain and decompressed value', function(){
        // Test that the decompression works, it doesn't matter if it's from localStorage or the dummy hash.

        var latinSpecialCharacters = "ÄäÀàÁáÂâÃãÅåǍǎĄąĂăÆæĀāÇçĆćĈĉČčĎđĐďðÈèÉéÊêËëĚěĘęĖėĒēĜĝĢģĞğĤĥÌìÍíÎîÏïıĪīĮį" +
            "ĴĵĶķĹĺĻļŁłĽľÑñŃńŇňŅņÖöÒòÓóÔôÕõ";
        var chineseSpecialCharacters = '汉语漢語华语';

        localStorage.setItem(lsKey('plain-latin'), jsonStringTestData(latinSpecialCharacters));
        deepEqual(clientStorage.getItem(lsKey('plain-latin')), jsonTestData(latinSpecialCharacters),
            'plain string value from previously set item retrieved');
        clientStorage.removeSessionItem(lsKey('plain-latin'));

        localStorage.setItem(lsKey('plain-chinese'), jsonStringTestData(chineseSpecialCharacters));
        deepEqual(clientStorage.getItem(lsKey('plain-chinese')), jsonTestData(chineseSpecialCharacters),
            'plain string value from previously set item retrieved');
        clientStorage.removeSessionItem(lsKey('plain-chinese'));
    });

    _test('SessionStorage', function(){

        equal(clientStorage.getSessionItem(lsKey()), testString, 'value from previously set item retrieved');
        // remove an item and test that it is removed
        clientStorage.removeSessionItem(lsKey());
        strictEqual(clientStorage.getSessionItem(lsKey()), null, 'A removed item should no longer be accessible');

    });

    _test('build keys', function(){

        equal(clientStorage.buildKey(['component1', 'component2']), 'component1_component2', 'Components are joined correctly');
        equal(clientStorage.buildKey([null, undefined]), '_', 'null and undefined are treated correctly');
        equal(clientStorage.buildKey([[[['hello'], 'world']]]), 'hello,world', 'Nested arrays are concatenated');
        equal(clientStorage.buildKey([{hello:'world'}, 'part2']), '[object Object]_part2', 'objects are stringified in keys');
        equal(clientStorage.buildKey('string'), 'string', 'Passing in a single string works');
        equal(clientStorage.buildKey([1]), 1, 'Passing in a number works');
        throws(function() { clientStorage.buildKey(); }, Error, 'Trying to create a buildKey with no arguments throws an Error');

        var component = 'comp';
        var contextKeys = {
            pr: 'pull-request-key',
            repo: 'repository-key',
            project: 'project-key',
            user: 'user-key'
        };
        // One object to mock them all
        var mock = {
            getId: function() { return contextKeys.pr; },
            getKey: function() { return contextKeys.project; },
            getSlug: function() { return contextKeys.repo; },
            getName: function() { return contextKeys.user; }
        };

        // Because of the fall through nature of the build key, the specificy is more specific for the first item in the switch statement.
        sinon.stub(pageState, 'getPullRequest', fn.constant(mock));
        sinon.stub(pageState, 'getProject', fn.constant(mock));
        sinon.stub(pageState, 'getRepository', fn.constant(mock));
        sinon.stub(pageState, 'getCurrentUser', fn.constant(mock));

        // We'll shift the first item off the context after each assertion and join the remainder
        var context = _.toArray(contextKeys);
        equal(clientStorage.buildKey(component, 'pull-request'), component + '_pull-request_' + context.join('_'), 'Pull Request level key generates correctly');
        context.shift();
        equal(clientStorage.buildKey(component, 'repo'), component + '_repo_' + context.join('_'), 'Repository level key generates correctly');
        context.shift();
        equal(clientStorage.buildKey(component, 'project'), component + '_project_' + context.join('_'), 'Project level key generates correctly');
        context.shift();
        equal(clientStorage.buildKey(component, 'user'), component + '_user_' + context.join('_'), 'User level key generates correctly');

        equal(clientStorage.buildKey(component, 'new-context'), component + '_new-context', 'Non-existing context simply gets appended.');

        pageState.getPullRequest.restore();
        pageState.getProject.restore();
        pageState.getRepository.restore();
        pageState.getCurrentUser.restore();

    });

    _asyncTest('Cleanup', function() {
        prepareForCleanup();

        // Now when we set a new item, we expect the old item to be removed.
        clientStorage.setItem(testItemKey, testString);

        // We need to defer our test execution because the clientStorage module defers the cleanup task.
        _.defer(function() {
            start();
            strictEqual(clientStorage.getItem(oldItemKey), null, 'old item is removed');
        });
    });

    _asyncTest('Cleanup - null key', function() {
        prepareForCleanup();

        var errorCount = 0;
        var oldErrorHandler = window.onerror;

        window.onerror = function(message, file, line) {
            oldErrorHandler(message, file, line);
            errorCount++;
        };

        localStorage.setItem(lsKey('error-test'), null);

        clientStorage.setItem(lsKey(), testString);

        //We need to defer our test execution because the clientStorage module defers the cleanup task.
        _.defer(function() {
            start();
            strictEqual(errorCount, 0, 'no errors detected when cleaning up client storage');
            // hand the error handler back just in case there was one.
            window.onerror = oldErrorHandler;
            localStorage.removeItem(lsKey('error-test'));
        });
    });

    _test('Flash items', function(){

        clientStorage.setFlashItem(lsKey('flash'), testString);

        equal(clientStorage.getFlashItem(lsKey('flash')), testString, 'Can find a flash item that was set.');
        strictEqual(clientStorage.getFlashItem(lsKey('flash')), null, 'The flash item should be removed after its first access.');

    });

    test('doCleanup', function() {

        function save(obj, key) {
            localStorage.setItem(lsKey(key), JSON.stringify(obj));
        }

        function fetch(key) {
            return JSON.parse(localStorage.getItem(lsKey(key)));
        }

        var obj = {timestamp: Date.now()};
        save(obj);
        clientStorage._doCleanup();
        var something = fetch();
        deepEqual(something, obj, "doesn't remove item if date is too new");

        obj = {timestamp: Date.now() - 1000};
        save(obj);
        clientStorage._doCleanup(1);
        deepEqual(fetch(), null, "removes item if timestamp is after minAge");

        obj = {timestamp: Date.now(), noCleanup: true};
        save(obj);
        clientStorage._doCleanup();
        deepEqual(fetch(), obj, "items marked as noCleanup aren't removed");

        obj = {test: true};
        save(obj);
        clientStorage._doCleanup();
        deepEqual(fetch(), obj, "items without a timestamp aren't removed");

        var oneDay = 1000 * 60 * 60 * 24;
        var oldObj = {timestamp: Date.now() - (15 * oneDay)};
        save(oldObj, 'old');
        var youngObj = {timestamp: Date.now() - (9 * oneDay)};
        save(youngObj, 'young');
        clientStorage._doCleanup();
        deepEqual(fetch('old'), null, "goes back to remove items if none were removed in the first pass");
        deepEqual(fetch('young'), youngObj, "does not remove younger objects if something else was removed first");

        clientStorage._doCleanup();
        deepEqual(fetch('young'), null, "removes objects older than one week if nothing older can be removed");

        youngObj = {timestamp: Date.now() - (6 * oneDay)};
        save(youngObj, 'young');
        clientStorage._doCleanup();
        deepEqual(fetch('young'), youngObj, "does not remove objects younger than one week even if nothing can be removed");
    });

});
