// TODO: These are really a mix of unit and integration tests.

var assert = require('assert');
var async = require('async');
var sinon = require('sinon');
var support = require('./support');
var check_err = support.check_err;
var caching = require('../index').caching;
var memory_store = require('../lib/stores/memory');

var methods = {
    get_widget: function (name, cb) {
        cb(null, {name: name});
    }
};

describe("caching", function () {
    var cache;
    var key;
    var ttl;
    var name;
    var value;

    describe("get() and set()", function () {
        ['memory'].forEach(function (store) {
            context("using " + store + " store", function () {
                beforeEach(function () {
                    cache = caching({store: store});
                    key = support.random.string(20);
                    value = support.random.string();
                });

                it("lets us set and get data in cache", function (done) {
                    cache.set(key, value, function (err) {
                        check_err(err);
                        cache.get(key, function (err, result) {
                            assert.equal(result, value);
                            done();
                        });
                    });
                });

                it("lets us set and get data without a callback", function (done) {
                    cache.set(key, value);
                    setTimeout(function () {
                        var result = cache.get(key);
                        assert.equal(result, value);
                        done();
                    }, 20);
                });
            });
        });
    });

    describe("del()", function () {
        ['memory'].forEach(function (store) {
            context("using " + store + " store", function () {
                beforeEach(function (done) {
                    cache = caching({store: store});
                    key = support.random.string(20);
                    value = support.random.string();
                    cache.set(key, value, function (err) {
                        check_err(err);
                        done();
                    });
                });

                it("deletes data from cache", function (done) {
                    cache.get(key, function (err, result) {
                        assert.equal(result, value);

                        cache.del(key, function (err) {
                            check_err(err);

                            cache.get(key, function (err, result) {
                                assert.ok(!result);
                                done();
                            });
                        });
                    });
                });

                it("lets us delete data without a callback", function (done) {
                    cache.get(key, function (err, result) {
                        assert.equal(result, value);

                        cache.del(key);

                        setTimeout(function () {
                            cache.get(key, function (err, result) {
                                assert.ok(!result);
                                done();
                            });
                        }, 20);
                    });
                });
            });
        });
    });

    describe("reset()", function () {
        var key2;
        var value2;

        beforeEach(function (done) {
            cache = caching({store: 'memory'});
            key = support.random.string(20);
            value = support.random.string();

            cache.set(key, value, function (err) {
                check_err(err);

                key2 = support.random.string(20);
                value2 = support.random.string();

                cache.set(key2, value2, done);
            });
        });

        it("clears the cache", function (done) {
            cache.reset(function (err) {
                check_err(err);

                cache.get(key, function (err, result) {
                    assert.ok(!result);

                    cache.get(key2, function (err, result) {
                        assert.ok(!result);
                        done();
                    });
                });
            });
        });

        it("lets us clear the cache without a callback (memory store only)", function (done) {
            cache.reset();
            setTimeout(function () {
                cache.get(key, function (err, result) {
                    assert.ok(!result);

                    cache.get(key2, function (err, result) {
                        assert.ok(!result);
                        done();
                    });
                });
            }, 10);
        });
    });

    describe("keys()", function () {
        var key_count;
        var saved_keys = [];

        beforeEach(function (done) {
            key_count = 10;
            var processed = 0;

            cache = caching({store: 'memory'});

            function is_done() {
                return processed === key_count;
            }

            async.until(is_done, function (cb) {
                processed += 1;
                key = support.random.string(20);
                saved_keys.push(key);
                value = support.random.string();
                cache.set(key, value, cb);
            }, done);
        });

        it("calls back with all keys in cache", function (done) {
            cache.keys(function (err, keys) {
                check_err(err);
                assert.deepEqual(keys.sort, saved_keys.sort);
                done();
            });
        });

        it("lets us get the keys without a callback (memory store only)", function () {
            var keys = cache.keys();
            assert.deepEqual(keys.sort, saved_keys.sort);
        });
    });

    describe("wrap()", function () {
        describe("using memory (lru-cache) store", function () {
            var memory_store_stub;

            beforeEach(function () {
                ttl = 0.1;
                memory_store_stub = memory_store.create({ttl: ttl});

                sinon.stub(memory_store, 'create').returns(memory_store_stub);

                cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: false});
                key = support.random.string(20);
                name = support.random.string();
            });

            afterEach(function () {
                memory_store.create.restore();
            });

            it("calls back with the result of the wrapped function", function (done) {
                cache.wrap(key, function (cb) {
                    methods.get_widget(name, cb);
                }, function (err, widget) {
                    check_err(err);
                    assert.deepEqual(widget, {name: name});
                    done();
                });
            });

            it("retrieves data from memory when available", function (done) {
                cache.wrap(key, function (cb) {
                    methods.get_widget(name, cb);
                }, function (err, widget) {
                    check_err(err);
                    assert.ok(widget);

                    memory_store_stub.get(key, function (err, result) {
                        check_err(err);
                        assert.ok(result);

                        sinon.spy(memory_store_stub, 'get');
                        var func_called = false;

                        cache.wrap(key, function (cb) {
                            methods.get_widget(name, function (err, result) {
                                func_called = true;
                                cb(err, result);
                            });
                        }, function (err, widget) {
                            check_err(err);
                            assert.deepEqual(widget, {name: name});
                            assert.ok(memory_store_stub.get.calledWith(key));
                            assert.ok(!func_called);
                            memory_store_stub.get.restore();
                            done();
                        });
                    });
                });
            });

            it("expires cached result after ttl seconds", function (done) {
                cache.wrap(key, function (cb) {
                    methods.get_widget(name, cb);
                }, function (err, widget) {
                    check_err(err);
                    assert.deepEqual(widget, {name: name});

                    memory_store_stub.get(key, function (err, result) {
                        check_err(err);
                        assert.ok(result);

                        var func_called = false;

                        setTimeout(function () {
                            cache.wrap(key, function (cb) {
                                methods.get_widget(name, function (err, result) {
                                    func_called = true;
                                    cb(err, result);
                                });
                            }, function (err, widget) {
                                check_err(err);
                                assert.ok(func_called);
                                assert.deepEqual(widget, {name: name});
                                done();
                            });
                        }, (ttl * 1000 + 10));
                    });
                });
            });

            context("when store.get() calls back with an error", function () {
                context("and ignoreCacheErrors is not set (default is false)", function () {
                    it("bubbles up that error", function (done) {
                        var fake_error = new Error(support.random.string());

                        sinon.stub(memory_store_stub, 'get', function (key, cb) {
                            cb(fake_error);
                        });

                        cache.wrap(key, function (cb) {
                            methods.get_widget(name, cb);
                        }, function (err) {
                            assert.equal(err, fake_error);
                            memory_store_stub.get.restore();
                            done();
                        });
                    });
                });

                context("and ignoreCacheErrors is set to true", function () {
                    it("does not bubble up that error", function (done) {
                        cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: true});

                        var fake_error = new Error(support.random.string());

                        sinon.stub(memory_store_stub, 'get', function (key, cb) {
                            cb(fake_error);
                        });

                        cache.wrap(key, function (cb) {
                            methods.get_widget(name, cb);
                        }, function (err) {
                            assert.equal(err, null);
                            memory_store_stub.get.restore();
                            done();
                        });
                    });
                });
            });

            context("when store.set() calls back with an error", function () {
                context("and ignoreCacheErrors is not set", function () {
                    it("bubbles up that error", function (done) {
                        var fake_error = new Error(support.random.string());

                        sinon.stub(memory_store_stub, 'set', function (key, val, cb) {
                            cb(fake_error);
                        });

                        cache.wrap(key, function (cb) {
                            methods.get_widget(name, cb);
                        }, function (err) {
                            assert.equal(err, fake_error);
                            memory_store_stub.set.restore();
                            done();
                        });
                    });
                });

                context("and ignoreCacheErrors is set to true", function () {
                    it("does not bubbles up that error", function (done) {
                        cache = caching({store: 'memory', ttl: ttl, ignoreCacheErrors: true});
                        var fake_error = new Error(support.random.string());

                        sinon.stub(memory_store_stub, 'set', function (key, val, cb) {
                            cb(fake_error);
                        });

                        cache.wrap(key, function (cb) {
                            methods.get_widget(name, cb);
                        }, function (err) {
                            assert.equal(err, null);
                            memory_store_stub.set.restore();
                            done();
                        });
                    });
                });
            });

            context("when wrapped function calls back with an error", function () {
                it("calls back with that error", function (done) {
                    var fake_error = new Error(support.random.string());
                    sinon.stub(methods, 'get_widget', function (name, cb) {
                        cb(fake_error, {name: name});
                    });

                    cache.wrap(key, function (cb) {
                        methods.get_widget(name, cb);
                    }, function (err, widget) {
                        methods.get_widget.restore();
                        assert.equal(err, fake_error);
                        assert.ok(!widget);
                        done();
                    });
                });
            });
        });
    });

    describe("instantiating with no store passed in", function () {
        it("defaults to 'memory' store", function () {
            var cache = caching();
            assert.equal(cache.store.name, 'memory');
        });
    });

    describe("instantiating with custom store", function () {
        it("allows us to pass in our own store object", function (done) {
            var store = memory_store.create({ttl: ttl});
            cache = caching({store: store});
            cache.set(key, value, function (err) {
                check_err(err);
                cache.get(key, function (err, result) {
                    assert.equal(result, value);
                    done();
                });
            });
        });

        it("allows us to pass in a path to our own store", function (done) {
            var store_path = '../lib/stores/memory';
            cache = caching({store: store_path});
            cache.set(key, value, function (err) {
                check_err(err);
                cache.get(key, function (err, result) {
                    assert.equal(result, value);
                    done();
                });
            });
        });

        it("allows us to pass in a module (uninstantiated)", function (done) {
            var store = memory_store;
            cache = caching({store: store});
            cache.set(key, value, function (err) {
                check_err(err);
                cache.get(key, function (err, result) {
                    assert.equal(result, value);
                    done();
                });
            });
        });
    });
});