var path = require('path');
var mout = require('mout');
var rimraf = require('rimraf');
var fs = require('graceful-fs');
var Q = require('q');
var expect = require('expect.js');
var mkdirp = require('mkdirp');
var ResolveCache = require('../../lib/core/ResolveCache');
var defaultConfig = require('../../lib/config');
var cmd = require('../../lib/util/cmd');
var copy = require('../../lib/util/copy');
var md5 = require('../../lib/util/md5');

describe('ResolveCache', function () {
    var resolveCache;
    var testPackage = path.resolve(__dirname, '../assets/package-a');
    var tempPackage = path.resolve(__dirname, '../tmp/temp-package');
    var tempPackage2 = path.resolve(__dirname, '../tmp/temp2-package');
    var cacheDir = path.join(__dirname, '../tmp/temp-resolve-cache');

    before(function (next) {
        // Delete cache folder
        rimraf.sync(cacheDir);

        // Instantiate resolver cache
        resolveCache = new ResolveCache(mout.object.deepMixIn(defaultConfig, {
            storage: {
                packages: cacheDir
            }
        }));

        // Checkout test package version 0.2.0
        cmd('git', ['checkout', '0.2.0'], { cwd: testPackage })
        .then(next.bind(next, null), next);
    });

    beforeEach(function () {
        // Reset in memory cache for each test
        resolveCache.reset();
    });

    after(function () {
        // Remove cache folder afterwards
        rimraf.sync(cacheDir);
    });

    describe('.constructor', function () {
        beforeEach(function () {
            // Delete temp folder
            rimraf.sync(tempPackage);
        });
        after(function () {
            // Delete temp folder
            rimraf.sync(tempPackage);
        });

        function initialize(cacheDir) {
            return new ResolveCache(mout.object.deepMixIn(defaultConfig, {
                storage: {
                    packages: cacheDir
                }
            }));
        }

        it('should create the cache folder if it doesn\'t exists', function () {
            initialize(tempPackage);
            expect(fs.existsSync(tempPackage)).to.be(true);
        });

        it('should not error out if the cache folder already exists', function () {
            mkdirp.sync(tempPackage);
            initialize(tempPackage);
        });
    });

    describe('.store', function () {
        var oldFsRename = fs.rename;

        beforeEach(function (next) {
            // Restore oldFsRename
            fs.rename = oldFsRename;

            // Create a fresh copy of the test package into temp
            rimraf.sync(tempPackage);
            copy.copyDir(testPackage, tempPackage, { ignore: ['.git'] })
            .then(next.bind(next, null), next);
        });

        it('should move the canonical dir to source-md5/version/ folder if package meta has a version', function (next) {
            resolveCache.store(tempPackage, {
                name: 'foo',
                version: '1.0.0',
                _source: 'foo',
                _target: '*'
            })
            .then(function (dir) {
                expect(dir).to.equal(path.join(cacheDir, md5('foo'), '1.0.0'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should move the canonical dir to source-md5/target/ folder if package meta has no version', function (next) {
            resolveCache.store(tempPackage, {
                name: 'foo',
                _source: 'foo',
                _target: 'some-branch'
            })
            .then(function (dir) {
                expect(dir).to.equal(path.join(cacheDir, md5('foo'), 'some-branch'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should move the canonical dir to source-md5/_wildcard/ folder if package meta has no version and target is *', function (next) {
            resolveCache.store(tempPackage, {
                name: 'foo',
                _source: 'foo',
                _target: '*'
            })
            .then(function (dir) {
                expect(dir).to.equal(path.join(cacheDir, md5('foo'), '_wildcard'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should read the package meta if not present', function (next) {
            var pkgMeta = path.join(tempPackage, '.bower.json');

            // Copy bower.json to .bower.json and add some props
            copy.copyFile(path.join(tempPackage, 'component.json'), pkgMeta)
            .then(function () {
                return Q.nfcall(fs.readFile, pkgMeta)
                .then(function (contents) {
                    var json = JSON.parse(contents.toString());

                    json._target = '~0.2.0';
                    json._source = 'git://github.com/bower/test-package.git';

                    return Q.nfcall(fs.writeFile, pkgMeta, JSON.stringify(json, null, '  '));
                });
            })
            // Store as usual
            .then(function () {
                return resolveCache.store(tempPackage);
            })
            .then(function (dir) {
                expect(dir).to.equal(path.join(cacheDir, md5('git://github.com/bower/test-package.git'), '0.2.0'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should error out when reading the package meta if the file does not exist', function (next) {
            resolveCache.store(tempPackage)
            .then(function () {
                next(new Error('Should have failed'));
            }, function (err) {
                expect(err).to.be.an(Error);
                expect(err.code).to.equal('ENOENT');
                expect(err.message).to.contain(path.join(tempPackage, '.bower.json'));

                next();
            })
            .done();
        });

        it('should error out when reading an invalid package meta', function (next) {
            var pkgMeta = path.join(tempPackage, '.bower.json');

            return Q.nfcall(fs.writeFile, pkgMeta, 'w00t')
            .then(function () {
                return resolveCache.store(tempPackage)
                .then(function () {
                    next(new Error('Should have failed'));
                }, function (err) {
                    expect(err).to.be.an(Error);
                    expect(err.code).to.equal('EMALFORMED');
                    expect(err.message).to.contain(path.join(tempPackage, '.bower.json'));

                    next();
                });
            })
            .done();
        });

        it('should move the canonical dir, even if it is in a different drive', function (next) {
            var hittedMock = false;

            fs.rename = function (src, dest, cb) {
                hittedMock = true;

                setTimeout(function () {
                    var err = new Error();
                    err.code = 'EXDEV';
                    cb(err);
                }, 10);
            };

            resolveCache.store(tempPackage, {
                name: 'foo',
                _source: 'foobar',
                _target: 'some-branch'
            })
            .then(function (dir) {
                // Ensure mock was called
                expect(hittedMock).to.be(true);

                expect(dir).to.equal(path.join(cacheDir, md5('foobar'), 'some-branch'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should update the in-memory cache', function (next) {
            // Feed the cache
            resolveCache.versions('test-in-memory')
            // Copy temp package to temp package  2
            .then(function () {
                return copy.copyDir(tempPackage, tempPackage2, { ignore: ['.git'] });
            })
            // Store the two packages
            .then(function () {
                return resolveCache.store(tempPackage, {
                    name: 'foo',
                    version: '1.0.0',
                    _source: 'test-in-memory',
                    _target: '*'
                });
            })
            .then(function () {
                return resolveCache.store(tempPackage2, {
                    name: 'foo',
                    version: '1.0.1',
                    _source: 'test-in-memory',
                    _target: '*'
                });
            })
            // Cache should have been updated
            .then(function () {
                return resolveCache.versions('test-in-memory')
                .then(function (versions) {
                    expect(versions).to.eql(['1.0.1', '1.0.0']);

                    next();
                });
            })
            .done();
        });

        it('should url encode target when storing to the fs', function (next) {
            resolveCache.store(tempPackage, {
                name: 'foo',
                _source: 'foo',
                _target: 'foo/bar'
            })
            .then(function (dir) {
                expect(dir).to.equal(path.join(cacheDir, md5('foo'), 'foo%2Fbar'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);

                next();
            })
            .done();
        });

        it('should be possible to store two package at same time', function (next) {
            var store = resolveCache.store.bind(resolveCache, tempPackage, {
                name: 'foo',
                _source: 'foo',
                _target: 'foo/bar'
            });
            var store2 = resolveCache.store.bind(resolveCache, tempPackage2, {
                name: 'foo',
                _source: 'foo',
                _target: 'foo/bar'
            });

            Q.all([store(), store2()]).then(function (dirs) {
                var dir = dirs[0];
                expect(dir).to.equal(path.join(cacheDir, md5('foo'), 'foo%2Fbar'));
                expect(fs.existsSync(dir)).to.be(true);
                expect(fs.existsSync(path.join(dir, 'baz'))).to.be(true);
                expect(fs.existsSync(tempPackage)).to.be(false);
                expect(fs.existsSync(tempPackage2)).to.be(false);

                next();
            }).done();
        });
    });

    describe('.versions', function () {
        it('should resolve to an array', function (next) {
            resolveCache.versions(String(Math.random()))
            .then(function (versions) {
                expect(versions).to.be.an('array');
                next();
            })
            .done();
        });

        it('should ignore non-semver folders of the source', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.mkdirSync(path.join(sourceDir, 'foo'));

            resolveCache.versions(source)
            .then(function (versions) {
                expect(versions).to.not.contain('foo');
                expect(versions).to.contain('0.0.1');
                expect(versions).to.contain('0.1.0');
                next();
            })
            .done();
        });

        it('should order the versions', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0-rc.1'));

            resolveCache.versions(source)
            .then(function (versions) {
                expect(versions).to.eql(['0.1.0', '0.1.0-rc.1', '0.0.1']);
                next();
            })
            .done();
        });

        it('should cache versions to speed-up subsequent calls', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            resolveCache.versions(source)
            .then(function () {
                // Remove folder
                rimraf.sync(sourceDir);

                return resolveCache.versions(source);
            })
            .then(function (versions) {
                expect(versions).to.eql(['0.0.1']);
                next();
            })
            .done();
        });
    });

    describe('.retrieve', function () {
        it('should resolve to empty if there are no packages for the requested source', function (next) {
            resolveCache.retrieve(String(Math.random()))
            .spread(function () {
                expect(arguments.length).to.equal(0);
                next();
            })
            .done();
        });

        it('should resolve to empty if there are no suitable packages for the requested target', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.mkdirSync(path.join(sourceDir, '0.1.9'));
            fs.mkdirSync(path.join(sourceDir, '0.2.0'));

            resolveCache.retrieve(source, '~0.3.0')
            .spread(function () {
                expect(arguments.length).to.equal(0);

                return resolveCache.retrieve(source, 'some-branch');
            })
            .spread(function () {
                expect(arguments.length).to.equal(0);

                next();
            })
            .done();
        });

        it('should remove invalid packages from the cache if their package meta is missing or invalid', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.mkdirSync(path.join(sourceDir, '0.1.9'));
            fs.mkdirSync(path.join(sourceDir, '0.2.0'));

            // Create an invalid package meta
            fs.writeFileSync(path.join(sourceDir, '0.2.0', '.bower.json'), 'w00t');

            resolveCache.retrieve(source, '~0.1.0')
            .spread(function () {
                var dirs = fs.readdirSync(sourceDir);

                expect(arguments.length).to.equal(0);
                expect(dirs).to.contain('0.0.1');
                expect(dirs).to.contain('0.2.0');
                next();
            })
            .done();
        });

        it('should resolve to the highest package that matches a range target, ignoring pre-releases', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = { name: 'foo' };

            // Create some versions
            fs.mkdirSync(sourceDir);

            json.version = '0.0.1';
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.writeFileSync(path.join(sourceDir, '0.0.1', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0';
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.writeFileSync(path.join(sourceDir, '0.1.0', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0-rc.1';
            fs.mkdirSync(path.join(sourceDir, '0.1.0-rc.1'));
            fs.writeFileSync(path.join(sourceDir, '0.1.0-rc.1', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.9';
            fs.mkdirSync(path.join(sourceDir, '0.1.9'));
            fs.writeFileSync(path.join(sourceDir, '0.1.9', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.2.0';
            fs.mkdirSync(path.join(sourceDir, '0.2.0'));
            fs.writeFileSync(path.join(sourceDir, '0.2.0', '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.retrieve(source, '~0.1.0')
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(pkgMeta.version).to.equal('0.1.9');
                expect(canonicalDir).to.equal(path.join(sourceDir, '0.1.9'));

                return resolveCache.retrieve(source, '*');
            })
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(pkgMeta.version).to.equal('0.2.0');
                expect(canonicalDir).to.equal(path.join(sourceDir, '0.2.0'));

                next();
            })
            .done();
        });

        it('should resolve to the highest package that matches a range target, not ignoring pre-releases if they are the only versions', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = { name: 'foo' };

            // Create some versions
            fs.mkdirSync(sourceDir);

            json.version = '0.1.0-rc.1';
            fs.mkdirSync(path.join(sourceDir, '0.1.0-rc.1'));
            fs.writeFileSync(path.join(sourceDir, '0.1.0-rc.1', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0-rc.2';
            fs.mkdirSync(path.join(sourceDir, '0.1.0-rc.2'));
            fs.writeFileSync(path.join(sourceDir, '0.1.0-rc.2', '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.retrieve(source, '~0.1.0')
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(pkgMeta.version).to.equal('0.1.0-rc.2');
                expect(canonicalDir).to.equal(path.join(sourceDir, '0.1.0-rc.2'));

                next();
            })
            .done();
        });

        it('should resolve to exact match (including build metadata) if available', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = { name: 'foo' };
            var encoded;

            // Create some versions
            fs.mkdirSync(sourceDir);

            json.version = '0.1.0';
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            fs.writeFileSync(path.join(sourceDir, '0.1.0', '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0+build.4';
            encoded = encodeURIComponent('0.1.0+build.4');
            fs.mkdirSync(path.join(sourceDir, encoded));
            fs.writeFileSync(path.join(sourceDir, encoded, '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0+build.5';
            encoded = encodeURIComponent('0.1.0+build.5');
            fs.mkdirSync(path.join(sourceDir, encoded));
            fs.writeFileSync(path.join(sourceDir, encoded, '.bower.json'), JSON.stringify(json, null, '  '));

            json.version = '0.1.0+build.6';
            encoded = encodeURIComponent('0.1.0+build.6');
            fs.mkdirSync(path.join(sourceDir, encoded));
            fs.writeFileSync(path.join(sourceDir, encoded, '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.retrieve(source, '0.1.0+build.5')
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(pkgMeta.version).to.equal('0.1.0+build.5');
                expect(canonicalDir).to.equal(path.join(sourceDir, encodeURIComponent('0.1.0+build.5')));

                next();
            })
            .done();
        });

        it('should resolve to the _wildcard package if target is * and there are no semver versions', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = { name: 'foo' };

            // Create some versions
            fs.mkdirSync(sourceDir);

            fs.mkdirSync(path.join(sourceDir, '_wildcard'));
            fs.writeFileSync(path.join(sourceDir, '_wildcard', '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.retrieve(source, '*')
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(canonicalDir).to.equal(path.join(sourceDir, '_wildcard'));

                next();
            })
            .done();
        });

        it('should resolve to the exact target it\'s not a semver range', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = { name: 'foo' };

            // Create some versions
            fs.mkdirSync(sourceDir);

            fs.mkdirSync(path.join(sourceDir, 'some-branch'));
            fs.writeFileSync(path.join(sourceDir, 'some-branch', '.bower.json'), JSON.stringify(json, null, '  '));

            fs.mkdirSync(path.join(sourceDir, 'other-branch'));
            fs.writeFileSync(path.join(sourceDir, 'other-branch', '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.retrieve(source, 'some-branch')
            .spread(function (canonicalDir, pkgMeta) {
                expect(pkgMeta).to.be.an('object');
                expect(pkgMeta).to.not.have.property('version');

                next();
            })
            .done();
        });
    });

    describe('.eliminate', function () {
        beforeEach(function () {
            mkdirp.sync(cacheDir);
        });

        it('should delete the source-md5/version folder', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '0.1.0'));

            resolveCache.eliminate({
                name: 'foo',
                version: '0.0.1',
                _source: source,
                _target: '*'
            })
            .then(function () {
                expect(fs.existsSync(path.join(sourceDir, '0.0.1'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, '0.1.0'))).to.be(true);

                next();
            })
            .done();
        });

        it('should delete the source-md5/target folder', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, 'some-branch'));

            resolveCache.eliminate({
                name: 'foo',
                _source: source,
                _target: 'some-branch'
            })
            .then(function () {
                expect(fs.existsSync(path.join(sourceDir, 'some-branch'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, '0.0.1'))).to.be(true);

                next();
            })
            .done();
        });

        it('should delete the source-md5/_wildcard folder', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(path.join(sourceDir, '_wildcard'));

            resolveCache.eliminate({
                name: 'foo',
                _source: source,
                _target: '*'
            })
            .then(function () {
                expect(fs.existsSync(path.join(sourceDir, '_wildcard'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, '0.0.1'))).to.be(true);

                next();
            })
            .done();
        });

        it('should delete the source-md5 folder if empty', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            resolveCache.eliminate({
                name: 'foo',
                version: '0.0.1',
                _source: source,
                _target: '*'
            })
            .then(function () {
                expect(fs.existsSync(path.join(sourceDir, '0.0.1'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir))).to.be(false);

                next();
            })
            .done();
        });

        it('should remove entry from in memory cache if the source-md5 folder was deleted', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            // Feed up the cache
            resolveCache.versions(source)
            // Eliminate
            .then(function () {
                return resolveCache.eliminate({
                    name: 'foo',
                    version: '0.0.1',
                    _source: source,
                    _target: '*'
                });
            })
            .then(function () {
                // At this point the parent folder should be deleted
                // To test against the in-memory cache, we create a folder
                // manually and request the versions
                mkdirp.sync(path.join(sourceDir, '0.0.2'));

                resolveCache.versions(source)
                .then(function (versions) {
                    expect(versions).to.eql(['0.0.2']);

                    next();
                });
            })
            .done();
        });
    });

    describe('.clear', function () {
        beforeEach(function () {
            mkdirp.sync(cacheDir);
        });

        it('should empty the whole cache folder', function (next) {
            resolveCache.clear()
            .then(function () {
                var files;

                expect(fs.existsSync(cacheDir)).to.be(true);

                files = fs.readdirSync(cacheDir);
                expect(files.length).to.be(0);

                next();
            })
            .done();
        });

        it('should erase the in-memory cache', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            // Feed the in-memory cache
            resolveCache.versions(source)
            // Clear
            .then(function () {
                return resolveCache.clear();
            })
            .then(function () {
                // To test against the in-memory cache, we create a folder
                // manually and request the versions
                mkdirp.sync(path.join(sourceDir, '0.0.2'));

                resolveCache.versions(source)
                .then(function (versions) {
                    expect(versions).to.eql(['0.0.2']);

                    next();
                });
            })
            .done();
        });
    });

    describe('.reset', function () {
        it('should erase the in-memory cache', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            // Feed the in-memory cache
            resolveCache.versions(source)
            .then(function () {
                // Delete 0.0.1 and create 0.0.2
                fs.rmdirSync(path.join(sourceDir, '0.0.1'));
                fs.mkdirSync(path.join(sourceDir, '0.0.2'));

                // Reset cache
                resolveCache.reset();

                // Get versions
                return resolveCache.versions(source);
            })
            .then(function (versions) {
                expect(versions).to.eql(['0.0.2']);

                next();
            })
            .done();
        });
    });

    describe('.list', function () {
        beforeEach(function () {
            rimraf.sync(cacheDir);
            mkdirp.sync(cacheDir);
        });

        it('should resolve to an empty array if the cache is empty', function (next) {
            resolveCache.list()
            .then(function (entries) {
                expect(entries).to.be.an('array');
                expect(entries.length).to.be(0);

                next();
            })
            .done();
        });

        it('should resolve to an ordered array of entries (name ASC, release ASC)', function (next) {
            var source = 'list-package-1';
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            var source2 = 'list-package-2';
            var sourceId2 = md5(source2);
            var sourceDir2 = path.join(cacheDir, sourceId2);

            var json = {
                name: 'foo'
            };

            // Create some versions for different sources
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            json.version = '0.0.1';
            fs.writeFileSync(path.join(sourceDir, '0.0.1', '.bower.json'), JSON.stringify(json, null, '  '));

            fs.mkdirSync(path.join(sourceDir, '0.1.0'));
            json.version = '0.1.0';
            fs.writeFileSync(path.join(sourceDir, '0.1.0', '.bower.json'), JSON.stringify(json, null, '  '));

            delete json.version;

            fs.mkdirSync(path.join(sourceDir, 'foo'));
            json._target = 'foo';
            fs.writeFileSync(path.join(sourceDir, 'foo', '.bower.json'), JSON.stringify(json, null, '  '));

            fs.mkdirSync(path.join(sourceDir, 'bar'));
            json._target = 'bar';
            fs.writeFileSync(path.join(sourceDir, 'bar', '.bower.json'), JSON.stringify(json, null, '  '));

            fs.mkdirSync(path.join(sourceDir, 'aa'));
            json._target = 'aa';
            fs.writeFileSync(path.join(sourceDir, 'aa', '.bower.json'), JSON.stringify(json, null, '  '));

            delete json._target;

            fs.mkdirSync(sourceDir2);
            fs.mkdirSync(path.join(sourceDir2, '0.2.1'));
            json.version = '0.2.1';
            fs.writeFileSync(path.join(sourceDir2, '0.2.1', '.bower.json'), JSON.stringify(json, null, '  '));

            fs.mkdirSync(path.join(sourceDir2, '0.2.0'));
            json.name = 'abc';
            json.version = '0.2.0';
            fs.writeFileSync(path.join(sourceDir2, '0.2.0', '.bower.json'), JSON.stringify(json, null, '  '));

            resolveCache.list()
            .then(function (entries) {
                var expectedJson;
                var bowerDir = path.join(__dirname, '../..');

                expect(entries).to.be.an('array');

                expectedJson = fs.readFileSync(path.join(__dirname, '../assets/resolve-cache/list-json-1.json'));
                expectedJson = expectedJson.toString().trim();

                mout.object.forOwn(entries, function (entry) {
                    // Trim absolute bower path from json
                    entry.canonicalDir = entry.canonicalDir.substr(bowerDir.length);
                    // Convert windows \ paths to /
                    entry.canonicalDir = entry.canonicalDir.replace(/\\/g, '/');
                });

                json = JSON.stringify(entries, null, '  ');
                expect(json).to.equal(expectedJson);

                next();
            })
            .done();
        });

        it('should ignore lurking files where dirs are expected', function (next) {
            var source = 'list-package-1';
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = {
                name: 'foo'
            };

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            json.version = '0.0.1';
            fs.writeFileSync(path.join(sourceDir, '0.0.1', '.bower.json'), JSON.stringify(json, null, '  '));

            // Create lurking files
            fs.writeFileSync(path.join(cacheDir, 'foo'), 'w00t');
            fs.writeFileSync(path.join(cacheDir, '.DS_Store'), '');
            fs.writeFileSync(path.join(sourceDir, 'foo'), 'w00t');
            fs.writeFileSync(path.join(sourceDir, '.DS_Store'), '');

            // It should not error out
            resolveCache.list()
            .then(function (entries) {
                expect(entries).to.be.an('array');
                expect(entries.length).to.be(1);
                expect(entries[0].pkgMeta).to.eql(json);

                // Lurking file should have been removed
                expect(fs.existsSync(path.join(cacheDir, 'foo'))).to.be(false);
                expect(fs.existsSync(path.join(cacheDir, '.DS_Store'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, 'foo'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, '.DS_Store'))).to.be(false);

                next();
            })
            .done();

        });

        it('should delete entries if failed to read package meta', function (next) {
            var source = 'list-package-1';
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);
            var json = {
                name: 'foo'
            };

            // Create invalid versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));

            fs.mkdirSync(path.join(sourceDir, '0.0.2'));
            fs.writeFileSync(path.join(sourceDir, '0.0.2', '.bower.json'), 'w00t');

            // Create valid version
            fs.mkdirSync(path.join(sourceDir, '0.0.3'));
            json.version = '0.0.3';
            fs.writeFileSync(path.join(sourceDir, '0.0.3', '.bower.json'), JSON.stringify(json, null, '  '));

            // It should not error out
            resolveCache.list()
            .then(function (entries) {
                expect(entries).to.be.an('array');
                expect(entries.length).to.be(1);
                expect(entries[0].pkgMeta).to.eql(json);

                // Packages with invalid metas should have been removed
                expect(fs.existsSync(path.join(sourceDir, '0.0.1'))).to.be(false);
                expect(fs.existsSync(path.join(sourceDir, '0.0.2'))).to.be(false);

                next();
            })
            .done();
        });
    });

    describe('#clearRuntimeCache', function () {
        it('should clear the in-memory cache for all sources', function (next) {
            var source = String(Math.random());
            var sourceId = md5(source);
            var sourceDir = path.join(cacheDir, sourceId);

            var source2 = String(Math.random());
            var sourceId2 = md5(source2);
            var sourceDir2 = path.join(cacheDir, sourceId2);

            // Create some versions
            fs.mkdirSync(sourceDir);
            fs.mkdirSync(path.join(sourceDir, '0.0.1'));
            fs.mkdirSync(sourceDir2);
            fs.mkdirSync(path.join(sourceDir2, '0.0.2'));

            // Feed the cache
            resolveCache.versions(source)
            .then(function () {
                return resolveCache.versions(source2);
            })
            .then(function () {
                // Create some more
                fs.mkdirSync(path.join(sourceDir, '0.0.3'));
                fs.mkdirSync(path.join(sourceDir2, '0.0.4'));

                // Reset cache
                ResolveCache.clearRuntimeCache();
            })
            .then(function () {
                return resolveCache.versions(source)
                .then(function (versions) {
                    expect(versions).to.eql(['0.0.3', '0.0.1']);

                    return resolveCache.versions(source2);
                })
                .then(function (versions) {
                    expect(versions).to.eql(['0.0.4', '0.0.2']);

                    next();
                });
            })
            .done();
        });
    });
});
