var should = require('should'),
    sinon = require('sinon'),
    testUtils = require('../../utils'),
    _ = require('lodash'),
    ObjectId = require('bson-objectid'),
    Promise = require('bluebird'),
    configUtils = require('../../utils/configUtils'),
    common = require('../../../server/lib/common'),
    db = require('../../../server/data/db'),
    models = require('../../../server/models'),
    PostAPI = require('../../../server/api/posts'),
    urlService = require('../../../server/services/url'),
    settingsCache = require('../../../server/services/settings/cache'),

    sandbox = sinon.sandbox.create();

describe('Post API', function () {
    var localSettingsCache = {};

    before(testUtils.teardown);
    after(testUtils.teardown);

    before(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));

    beforeEach(function () {
        sandbox.stub(settingsCache, 'get').callsFake(function (key) {
            return localSettingsCache[key];
        });
    });

    afterEach(function () {
        sandbox.restore();
        localSettingsCache = {};
    });

    should.exist(PostAPI);

    describe('Browse', function () {
        beforeEach(function () {
            localSettingsCache.permalinks = '/:slug/';
        });

        afterEach(function () {
            configUtils.restore();
        });

        it('can fetch all posts with internal context in correct order', function (done) {
            PostAPI.browse({context: {internal: true}}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(8);

                results.posts[0].status.should.eql('scheduled');

                results.posts[1].status.should.eql('draft');
                results.posts[2].status.should.eql('draft');

                results.posts[3].status.should.eql('published');
                results.posts[4].status.should.eql('published');
                results.posts[5].status.should.eql('published');
                results.posts[6].status.should.eql('published');
                results.posts[7].status.should.eql('published');

                done();
            }).catch(done);
        });

        it('can fetch featured posts for user 1', function (done) {
            PostAPI.browse(_.merge({filter: 'featured:true'}, testUtils.context.owner)).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(2);
                results.posts[0].featured.should.eql(true);
                done();
            }).catch(done);
        });

        it('can fetch featured posts for user 2', function (done) {
            PostAPI.browse(_.merge({filter: 'featured:true'}, testUtils.context.admin)).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(2);
                results.posts[0].featured.should.eql(true);
                done();
            }).catch(done);
        });

        it('can exclude featured posts for user 1', function (done) {
            PostAPI.browse(_.merge({
                status: 'all',
                filter: 'featured:false'
            }, testUtils.context.owner)).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(4);
                results.posts[0].featured.should.eql(false);

                done();
            }).catch(done);
        });

        it('can limit the number of posts', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', limit: 3}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(3);
                results.meta.pagination.limit.should.eql(3);

                done();
            }).catch(done);
        });

        it('can fetch only static posts', function (done) {
            PostAPI.browse({context: {user: 1}, staticPages: true}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(1);
                results.posts[0].page.should.eql(true);

                done();
            }).catch(done);
        });

        it('can fetch only static posts with string \'true\'', function (done) {
            PostAPI.browse({context: {user: 1}, staticPages: 'true'}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(1);
                results.posts[0].page.should.eql(true);

                done();
            }).catch(done);
        });

        it('can fetch only static posts with string \'1\'', function (done) {
            PostAPI.browse({context: {user: 1}, staticPages: '1'}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(1);
                results.posts[0].page.should.eql(true);

                done();
            }).catch(done);
        });

        it('can exclude static posts', function (done) {
            PostAPI.browse({context: {user: 1}, staticPages: false}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(4);
                results.posts[0].page.should.eql(false);

                done();
            }).catch(done);
        });

        it('can fetch static and normal posts', function (done) {
            PostAPI.browse({context: {user: 1}, staticPages: 'all'}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(5);

                done();
            }).catch(done);
        });

        it('can fetch static and normal posts (filter version)', function (done) {
            PostAPI.browse({context: {user: 1}, filter: 'page:[false,true]'}).then(function (results) {
                // should be the same as the current staticPages: 'all'
                should.exist(results.posts);
                results.posts.length.should.eql(5);
                done();
            }).catch(done);
        });

        it('can fetch page 1', function (done) {
            PostAPI.browse({context: {user: 1}, page: 1, limit: 2, status: 'all'}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(2);
                results.posts[0].slug.should.eql('scheduled-post');
                results.posts[1].slug.should.eql('unfinished');
                results.meta.pagination.page.should.eql(1);
                results.meta.pagination.next.should.eql(2);

                done();
            }).catch(done);
        });

        it('can fetch page 2', function (done) {
            PostAPI.browse({context: {user: 1}, page: 2, limit: 2, status: 'all'}).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(2);
                results.posts[0].slug.should.eql('not-so-short-bit-complex');
                results.posts[1].slug.should.eql('short-and-sweet');
                results.meta.pagination.page.should.eql(2);
                results.meta.pagination.next.should.eql(3);
                results.meta.pagination.prev.should.eql(1);

                done();
            }).catch(done);
        });

        it('without context.user cannot fetch all posts', function (done) {
            PostAPI.browse({status: 'all'}).then(function (results) {
                should.not.exist(results);

                done(new Error('should not provide results if invalid status provided'));
            }).catch(function (err) {
                err.errorType.should.eql('NoPermissionError');
                done();
            });
        });

        it('without context.user cannot fetch draft posts', function (done) {
            PostAPI.browse({status: 'draft'}).then(function (results) {
                should.not.exist(results);

                done(new Error('should not provide results if invalid status provided'));
            }).catch(function (err) {
                err.errorType.should.eql('NoPermissionError');
                done();
            });
        });

        it('without context.user cannot use uuid to fetch draft posts in browse', function (done) {
            PostAPI.browse({status: 'draft', uuid: 'imastring'}).then(function (results) {
                should.not.exist(results);

                done(new Error('should not provide results if invalid status provided'));
            }).catch(function (err) {
                err.errorType.should.eql('NoPermissionError');
                done();
            });
        });

        it('with context.user can fetch drafts', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'draft'}).then(function (results) {
                should.exist(results);
                testUtils.API.checkResponse(results, 'posts');
                should.exist(results.posts);
                results.posts.length.should.eql(1);
                results.posts[0].status.should.eql('draft');
                testUtils.API.checkResponse(results.posts[0], 'post');

                done();
            }).catch(done);
        });

        it('with context.user can fetch all posts', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all'}).then(function (results) {
                should.exist(results);
                testUtils.API.checkResponse(results, 'posts');
                should.exist(results.posts);

                // DataGenerator creates 6 posts by default + 2 static pages
                results.posts.length.should.eql(6);
                testUtils.API.checkResponse(results.posts[0], 'post');
                done();
            }).catch(done);
        });

        it('can include tags', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', include: 'tags'}).then(function (results) {
                results.posts[0].tags.length.should.eql(0);
                results.posts[1].tags.length.should.eql(1);

                results.posts[1].tags[0].name.should.eql('pollo');
                done();
            }).catch(done);
        });

        it('[DEPRECATED] can include author (using status:all)', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', include: 'author'}).then(function (results) {
                should.exist(results.posts);
                should.exist(results.posts[0].author.name);
                results.posts[0].author.name.should.eql('Joe Bloggs');

                done();
            }).catch(done);
        });

        it('[DEPRECATED] can include author', function (done) {
            PostAPI.read({
                context: {user: testUtils.DataGenerator.Content.users[1].id},
                id: testUtils.DataGenerator.Content.posts[1].id,
                include: 'author'
            }).then(function (results) {
                should.exist(results.posts[0].author.name);
                results.posts[0].author.name.should.eql('Joe Bloggs');
                done();
            }).catch(done);
        });

        it('can include authors', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', include: 'authors'}).then(function (results) {
                should.exist(results.posts);
                should.exist(results.posts[0].authors);
                should.exist(results.posts[0].authors[0]);
                results.posts[0].authors[0].name.should.eql('Joe Bloggs');

                done();
            }).catch(done);
        });

        it('can fetch all posts for a tag', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                filter: 'tags:kitchen-sink',
                include: 'tags'
            }).then(function (results) {
                results.posts.length.should.be.eql(2);

                _.each(results.posts, function (post) {
                    var slugs = _.map(post.tags, 'slug');
                    slugs.should.containEql('kitchen-sink');
                });

                done();
            }).catch(done);
        });

        it('can include authors and be case insensitive', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', include: 'Authors'}).then(function (results) {
                should.exist(results.posts);
                should.exist(results.posts[0].authors);
                should.exist(results.posts[0].authors[0]);
                results.posts[0].authors[0].name.should.eql('Joe Bloggs');

                done();
            }).catch(done);
        });

        it('can include authors and ignore space in include', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', include: ' authors'}).then(function (results) {
                should.exist(results.posts);
                should.exist(results.posts[0].authors);
                should.exist(results.posts[0].authors[0]);
                results.posts[0].authors[0].name.should.eql('Joe Bloggs');

                done();
            }).catch(done);
        });

        it('[DEPRECATED] can fetch all posts for an author', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                filter: 'author:joe-bloggs',
                include: 'author'
            }).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(6);

                _.each(results.posts, function (post) {
                    post.author.slug.should.eql('joe-bloggs');
                });

                done();
            }).catch(done);
        });

        it('can fetch all posts for an author', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                filter: 'authors:joe-bloggs',
                include: 'authors'
            }).then(function (results) {
                should.exist(results.posts);
                results.posts.length.should.eql(6);

                _.each(results.posts, function (post) {
                    post.primary_author.slug.should.eql('joe-bloggs');
                });

                _.find(results.posts, {id: testUtils.DataGenerator.forKnex.posts[0].id}).authors.length.should.eql(1);
                _.find(results.posts, {id: testUtils.DataGenerator.forKnex.posts[3].id}).authors.length.should.eql(2);

                done();
            }).catch(done);
        });

        // @TODO: ensure filters are fully validated
        it.skip('cannot fetch all posts for a tag with invalid slug', function (done) {
            PostAPI.browse({filter: 'tags:invalid!'}).then(function () {
                done(new Error('Should not return a result with invalid tag'));
            }).catch(function (err) {
                should.exist(err);
                err.message.should.eql('Validation (isSlug) failed for tag');
                err.statusCode.should.eql(422);
                done();
            });
        });

        it.skip('cannot fetch all posts for an author with invalid slug', function (done) {
            PostAPI.browse({filter: 'authors:invalid!'}).then(function () {
                done(new Error('Should not return a result with invalid author'));
            }).catch(function (err) {
                should.exist(err);
                err.message.should.eql('Validation (isSlug) failed for author');
                err.statusCode.should.eql(422);
                done();
            });
        });

        it('with context.user can fetch a single field', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', limit: 5, fields: 'title'}).then(function (results) {
                should.exist(results.posts);

                should.exist(results.posts[0].title);
                should.not.exist(results.posts[0].slug);

                done();
            }).catch(done);
        });

        it('with context.user can fetch multiple fields', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                limit: 5,
                fields: 'slug,published_at'
            }).then(function (results) {
                should.exist(results.posts);

                should.exist(results.posts[0].published_at);
                should.exist(results.posts[0].slug);
                should.not.exist(results.posts[0].title);

                done();
            }).catch(done);
        });

        it('with context.user can fetch url and author fields', function (done) {
            sandbox.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[7].id).returns('/html-ipsum/');

            PostAPI.browse({context: {user: 1}, status: 'all', limit: 5}).then(function (results) {
                should.exist(results.posts);

                should.exist(results.posts[0].url);
                should.notEqual(results.posts[0].url, 'undefined');
                should.exist(results.posts[0].author);

                done();
            }).catch(done);
        });

        it('with context.user can fetch multiple fields and be case insensitive', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                limit: 5,
                fields: 'Slug,Published_At'
            }).then(function (results) {
                should.exist(results.posts);

                should.exist(results.posts[0].published_at);
                should.exist(results.posts[0].slug);
                should.not.exist(results.posts[0].title);

                done();
            }).catch(done);
        });

        it('with context.user can fetch multiple fields ignoring spaces', function (done) {
            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                limit: 5,
                fields: ' slug , published_at  '
            }).then(function (results) {
                should.exist(results.posts);

                should.exist(results.posts[0].published_at);
                should.exist(results.posts[0].slug);
                should.not.exist(results.posts[0].title);

                done();
            }).catch(done);
        });

        it('with context.user can fetch a field and not return invalid field', function (done) {
            PostAPI.browse({context: {user: 1}, status: 'all', limit: 5, fields: 'foo,title'}).then(function (results) {
                var objectKeys;
                should.exist(results.posts);

                should.exist(results.posts[0].title);
                should.not.exist(results.posts[0].foo);
                objectKeys = _.keys(results.posts[0]);
                objectKeys.length.should.eql(1);

                done();
            }).catch(done);
        });

        it('can order posts using asc', function (done) {
            var posts, expectedTitles;

            posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
            expectedTitles = _(posts).map('title').sortBy().value();

            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                order: 'title asc',
                fields: 'title'
            }).then(function (results) {
                should.exist(results.posts);

                var titles = _.map(results.posts, 'title');
                titles.should.eql(expectedTitles);

                done();
            }).catch(done);
        });

        it('can order posts using desc', function (done) {
            var posts, expectedTitles;

            posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
            expectedTitles = _(posts).map('title').sortBy().reverse().value();

            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                order: 'title DESC',
                fields: 'title'
            }).then(function (results) {
                should.exist(results.posts);

                var titles = _.map(results.posts, 'title');
                titles.should.eql(expectedTitles);

                done();
            }).catch(done);
        });

        it('can order posts and filter disallowed attributes', function (done) {
            var posts, expectedTitles;

            posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
            expectedTitles = _(posts).map('title').sortBy().value();

            PostAPI.browse({
                context: {user: 1},
                status: 'all',
                order: 'bunny DESC, title ASC',
                fields: 'title'
            }).then(function (results) {
                should.exist(results.posts);

                var titles = _.map(results.posts, 'title');
                titles.should.eql(expectedTitles);

                done();
            }).catch(done);
        });
    });

    describe('Read', function () {
        it('can fetch a post', function (done) {
            var firstPost;

            PostAPI.browse().then(function (results) {
                should.exist(results);
                should.exist(results.posts);
                results.posts.length.should.be.above(0);
                firstPost = _.find(results.posts, {title: testUtils.DataGenerator.Content.posts[0].title});
                return PostAPI.read({slug: firstPost.slug, include: 'tags'});
            }).then(function (found) {
                var post;

                should.exist(found);
                testUtils.API.checkResponse(found.posts[0], 'post', 'tags');

                post = found.posts[0];

                post.created_at.should.be.an.instanceof(Date);

                should.exist(post.tags);
                post.tags.length.should.be.above(0);
                testUtils.API.checkResponse(post.tags[0], 'tag');

                done();
            }).catch(done);
        });

        it('without context.user cannot fetch draft', function (done) {
            PostAPI.read({slug: 'unfinished', status: 'draft'}).then(function () {
                done(new Error('Should not return a result with no permission'));
            }).catch(function (err) {
                should.exist(err);
                err.errorType.should.eql('NoPermissionError');
                done();
            }).catch(done);
        });

        it('with context.user can fetch a draft', function (done) {
            PostAPI.read({context: {user: 1}, slug: 'unfinished', status: 'draft'}).then(function (results) {
                should.exist(results.posts);
                results.posts[0].status.should.eql('draft');

                done();
            }).catch(done);
        });

        it('without context.user can fetch a draft if uuid is provided', function (done) {
            PostAPI.read({uuid: 'd52c42ae-2755-455c-80ec-70b2ec55c903', status: 'draft'}).then(function (results) {
                should.exist(results.posts);
                results.posts[0].slug.should.eql('unfinished');
                done();
            }).catch(done);
        });

        it('cannot fetch post with unknown id', function (done) {
            PostAPI.read({context: {user: 1}, slug: 'not-a-post'}).then(function () {
                done(new Error('Should not return a result with unknown id'));
            }).catch(function (err) {
                should.exist(err);
                err.message.should.eql('Post not found.');

                done();
            }).catch(done);
        });

        it('can fetch post with by id', function (done) {
            PostAPI.read({
                context: {user: testUtils.DataGenerator.Content.users[1].id},
                id: testUtils.DataGenerator.Content.posts[1].id,
                status: 'all'
            }).then(function (results) {
                should.exist(results.posts);
                results.posts[0].id.should.eql(testUtils.DataGenerator.Content.posts[1].id);
                results.posts[0].slug.should.eql('ghostly-kitchen-sink');
                done();
            }).catch(done);
        });

        it('can include tags', function (done) {
            PostAPI.read({
                context: {user: testUtils.DataGenerator.Content.users[1].id},
                id: testUtils.DataGenerator.Content.posts[2].id,
                include: 'tags'
            }).then(function (results) {
                should.exist(results.posts[0].tags);
                results.posts[0].tags[0].slug.should.eql('chorizo');
                done();
            }).catch(done);
        });

        // TODO: this should be a 422?
        it('cannot fetch a post with an invalid slug', function (done) {
            PostAPI.read({slug: 'invalid!'}).then(function () {
                done(new Error('Should not return a result with invalid slug'));
            }).catch(function (err) {
                should.exist(err);
                err.message.should.eql('Post not found.');

                done();
            });
        });
    });

    describe('Destroy', function () {
        beforeEach(testUtils.teardown);
        beforeEach(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));
        after(testUtils.teardown);

        it('can delete a post', function (done) {
            var options = {
                context: {user: testUtils.DataGenerator.Content.users[1].id},
                id: testUtils.DataGenerator.Content.posts[0].id
            };

            PostAPI.read(options).then(function (results) {
                should.exist(results.posts[0]);

                return PostAPI.destroy(options);
            }).then(function (results) {
                should.not.exist(results);

                return PostAPI.read(options);
            }).then(function () {
                done(new Error('Post still exists when it should have been deleted'));
            }).catch(function () {
                done();
            });
        });

        it('returns an error when attempting to delete a non-existent post', function (done) {
            var options = {context: {user: testUtils.DataGenerator.Content.users[1].id}, id: ObjectId.generate()};

            PostAPI.destroy(options).then(function () {
                done(new Error('No error was thrown'));
            }).catch(function (error) {
                error.errorType.should.eql('NotFoundError');

                done();
            });
        });
    });

    describe('Edit', function () {
        beforeEach(testUtils.teardown);
        beforeEach(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));
        after(testUtils.teardown);

        it('can edit own post', function (done) {
            PostAPI.edit({posts: [{status: 'test'}]}, {
                context: {user: testUtils.DataGenerator.Content.users[1].id},
                id: testUtils.DataGenerator.Content.posts[0].id
            }).then(function (results) {
                should.exist(results.posts);
                done();
            }).catch(done);
        });

        it('cannot edit others post', function (done) {
            PostAPI.edit(
                {posts: [{status: 'test'}]},
                {
                    context: {user: testUtils.DataGenerator.Content.users[3].id},
                    id: testUtils.DataGenerator.Content.posts[0].id
                }
            ).then(function () {
                done(new Error('expected permission error'));
            }).catch(function (err) {
                should.exist(err);
                (err instanceof common.errors.NoPermissionError).should.eql(true);
                done();
            });
        });
    });
});
