import { clone, each, extend, range, times } from 'lodash';
import { Collection } from "../src/collection";
import { getSyncMethod, sync } from '../src/helpers.js';
import { Model } from '../src/model.js';
import { assert } from 'chai';
import Storage from '../src/storage.js';
import root from 'window-or-global';


describe("Storage using localStorage", function () {

    const attributes = {
        string: "String",
        string2: "String 2",
        number: 1337
    };

    const onError = function (model, resp, options) {
        throw new Error(resp);
    };

    describe("on a Collection", function () {
        beforeEach(() => localStorage.clear());

        const TestModel = Model.extend({
            defaults: attributes
        });

        const TestCollection = Collection.extend({
            model: TestModel,
            browserStorage: new Storage("collectionStore", "local")
        });

        it("should use `localSync`", function () {
            const collection = new TestCollection();
            collection.fetch();
            const method = getSyncMethod(collection);
            assert.equal(method.__name__, 'localSync');
        });

        it("should initially be empty", function () {
            const collection = new TestCollection();
            collection.fetch();
            assert.equal(collection.length, 0);
        });


        describe("create", function () {
            beforeEach(() => localStorage.clear());

            it("should have 1 model", async function () {
                const collection = new TestCollection();
                const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                assert.equal(collection.length, 1);
            });

            it("should have a populated model", async function () {
                const collection = new TestCollection();
                const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                assert.equal(collection.length, 1);
                assert.deepEqual(model.toJSON(), extend(clone(attributes), {'id': model.id}));
            });

            it("should have assigned an `id` to the model", async function () {
                const collection = new TestCollection();
                const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                await model.collection.browserStorage.storeInitialized;
                assert.isDefined(model.id);
            });

            it("should be saved to the localstorage", async function () {
                const collection = new TestCollection();
                const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                await model.collection.browserStorage.storeInitialized;
                assert.isNotNull(root.localStorage.getItem('localforage/collectionStore'+'-'+model.id));
            });
        });

        describe("get (by `id`)", function () {
            beforeEach(() => localStorage.clear());

            it("should find the model with its `id`", async function () {
                const collection = new TestCollection();
                const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                await model.collection.browserStorage.storeInitialized;
                assert.deepEqual(collection.get(model.id), model);
            });
        });

        describe("instances", function () {
            beforeEach(() => localStorage.clear());

            describe("when saved", function () {
                beforeEach(() => localStorage.clear());

                it("should persist the changes", async function () {
                    const collection = new TestCollection();
                    const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                    model.save({'string': "String 0"});
                    collection.fetch();

                    assert.equal(model.get("string"), "String 0");
                });

                describe("with a new `id`", function () {
                    beforeEach(() => localStorage.clear());

                    it("should have a new `id`", async function () {
                        const collection = new TestCollection();
                        const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                        model.save({'id': 1});
                        collection.fetch();

                        assert.equal(model.id, 1);
                    });

                    it("should have kept its old properties", async function () {
                        const collection = new TestCollection();
                        const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                        model.save({'id': 1});
                        collection.fetch();

                        const withId = clone(attributes);
                        withId.id = 1;
                        assert.deepEqual(model.toJSON(), withId);
                    });

                    it("should be saved in localstorage by new id", async function () {
                        const collection = new TestCollection();
                        const model = await new Promise((resolve, reject) => collection.create({}, {'success': resolve}));
                        model.save({'id': 1});
                        await new Promise((resolve, reject) => collection.fetch({'success': resolve}));
                        assert.isNotNull(root.localStorage.getItem('localforage/collectionStore-1'));
                    });
                });
            });


            describe("destroy", function () {
                beforeEach(() => localStorage.clear());

                it("should remove all items from the collection and its store", async function () {
                    const collection = new TestCollection();
                    await Promise.all(range(5).map(i => new Promise((resolve, reject) => collection.create({}, {'success': resolve}))));
                    assert.equal(collection.length, 5);
                    while (collection.length) {
                        collection.at(0).destroy();
                    }
                    const beforeFetchLength = collection.length;
                    collection.fetch();
                    const afterFetchLength = collection.length;

                    assert.equal(beforeFetchLength, 0);
                    assert.equal(afterFetchLength, 0);
                });
            });

            describe("with a different `idAttribute`", function () {

                it("should use the custom `idAttribute`", async function () {
                    const TestModel = Model.extend({
                        defaults: attributes,
                        idAttribute: "_id"
                    });
                    const TestCollection = Collection.extend({
                        model: TestModel,
                        browserStorage: new Storage("collection2Store", "local")
                    });

                    const collection = new TestCollection();
                    const model = await new Promise(resolve => collection.create({}, {'success': resolve}));
                    assert.equal(collection.first().id, collection.first().get("_id"));
                });
            });
        });
    });

    describe("on a Model", function () {
        beforeEach(() => localStorage.clear());

        const TestModel = Model.extend({
            defaults: attributes,
            browserStorage: new Storage("modelStore", "local")
        });

        it("should use `localSync`", function () {
            const model = new TestModel();
            assert.equal(getSyncMethod(model).__name__, 'localSync');
        });

        describe("fetch", function () {
            beforeEach(() => localStorage.clear());

            it('should fire sync event on fetch', function(done) {
                const model = new TestModel(attributes);
                model.on('sync', () => done());
                model.fetch();
            });
        });

        describe("save", function () {
            beforeEach(() => localStorage.clear());

            it("should have assigned an `id` to the model", async function () {
                const model = new TestModel();
                await new Promise((resolve, reject) => model.save(null, {'success': resolve}));
                model.fetch();
                assert.isDefined(model.id);
            });

            it("should be saved to the localstorage", async function () {
                const model = new TestModel();
                await new Promise((resolve, reject) => model.save(null, {'success': resolve}));
                assert.isNotNull(root.localStorage.getItem('localforage/modelStore'+'-'+model.id));
            });

            describe("with new attributes", function () {

                it("should persist the changes", async function () {
                    const model = new TestModel();
                    await new Promise((resolve, reject) => model.save({number: 42}, {'success': resolve}));
                    model.fetch();
                    assert.deepEqual(model.toJSON(), extend(clone(attributes), {id: model.id, number: 42}));
                });
            });

            describe('fires events', function () {

                it('should fire sync event on save', function(done) {
                    const model = new TestModel();
                    model.on('sync', () => done());
                    model.save({foo: 'baz'});
                });
            });
        });

        describe("destroy", function () {

            it("should have removed the instance from the store", async function () {
                const model = new TestModel();
                await new Promise((resolve, reject) => model.save(null, {'success': resolve}));
                const store = model.browserStorage.store;
                let item = await store.getItem(model.browserStorage.getItemName(model.id));
                assert.isNotNull(item);
                await new Promise((resolve, reject) => model.destroy({'success': resolve}));
                item = await store.getItem(model.browserStorage.getItemName(model.id));
                assert.isNull(item);
            });
        });
    });
});

describe("Without browserStorage", function () {
    beforeEach(() => localStorage.clear());

    describe("on a Collection", function () {

        it("should use `ajaxSync`", function () {
            const TestCollection = Collection.extend();
            const collection = new TestCollection();
            const method = getSyncMethod(collection);
            assert.equal(method, sync);
        });
    });

    describe("on a Model", function () {

        it("should use `ajaxSync`", function () {
            const TestModel = Model.extend();
            const model = new TestModel();
            const method = getSyncMethod(model);
            assert.equal(method, sync);
        });
    });
});