const assert = require('assert/strict');
const cheerio = require('cheerio');
const moment = require('moment');
const testUtils = require('../../utils');
const models = require('../../../core/server/models');

const {agentProvider, fixtureManager, matchers, mockManager} = require('../../utils/e2e-framework');
const {anyArray, anyContentVersion, anyEtag, anyUuid, anyISODateTimeWithTZ} = matchers;

const postMatcher = {
    published_at: anyISODateTimeWithTZ,
    created_at: anyISODateTimeWithTZ,
    updated_at: anyISODateTimeWithTZ,
    uuid: anyUuid
};

const postMatcherShallowIncludes = Object.assign(
    {},
    postMatcher, {
        tags: anyArray,
        authors: anyArray
    }
);

describe('Posts Content API', function () {
    let agent;

    before(async function () {
        // NOTE: can be removed after collections -> GA
        mockManager.mockLabsEnabled('collections');
        agent = await agentProvider.getContentAPIAgent();
        await fixtureManager.init('owner:post', 'users', 'user:inactive', 'posts', 'tags:extra', 'api_keys', 'newsletters', 'members:newsletters');
        await agent.authenticate();

        // Assign a newsletter to one of the posts
        const newsletterId = testUtils.DataGenerator.Content.newsletters[0].id;
        const postId = testUtils.DataGenerator.Content.posts[0].id;
        await models.Post.edit({newsletter_id: newsletterId}, {id: postId});
    });

    it('Can request posts', async function () {
        const res = await agent.get('posts/')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(11)
                    .fill(postMatcher)
            });

        assert.equal(res.body.posts[0].slug, 'welcome', 'Default order "published_at desc" check');
        assert.equal(res.body.posts[6].slug, 'integrations', 'Default order "published_at desc" check');

        // kitchen sink
        assert.equal(res.body.posts[9].slug, fixtureManager.get('posts', 1).slug);

        let urlParts = new URL(res.body.posts[9].feature_image);
        assert.equal(urlParts.protocol, 'http:');
        assert.equal(urlParts.host, '127.0.0.1:2369');

        urlParts = new URL(res.body.posts[9].url);
        assert.equal(urlParts.protocol, 'http:');
        assert.equal(urlParts.host, '127.0.0.1:2369');

        const $ = cheerio.load(res.body.posts[9].html);
        urlParts = new URL($('img').attr('src'));
        assert.equal(urlParts.protocol, 'http:');
        assert.equal(urlParts.host, '127.0.0.1:2369');

        assert.equal(res.body.posts[7].slug, 'not-so-short-bit-complex');
        assert.match(res.body.posts[7].html, /<a href="http:\/\/127.0.0.1:2369\/about#nowhere" title="Relative URL/);
        assert.equal(res.body.posts[9].slug, 'ghostly-kitchen-sink');
        assert.match(res.body.posts[9].html, /<img src="http:\/\/127.0.0.1:2369\/content\/images\/lol.jpg"/);
    });

    it('Cannot request mobiledoc or lexical formats', async function () {
        await agent
            .get(`posts/?formats=mobiledoc,lexical`)
            .expectStatus(200)
            .matchBodySnapshot({
                posts: new Array(11).fill(postMatcher)
            });
    });

    it('Cannot request mobiledoc or lexical fields', async function () {
        await agent
            .get(`posts/?fields=mobiledoc,lexical,published_at,created_at,updated_at,uuid`)
            .expectStatus(200)
            .matchBodySnapshot({
                posts: new Array(11).fill(postMatcher)
            });
    });

    it('Can filter posts by tag', async function () {
        const res = await agent.get('posts/?filter=tag:kitchen-sink,featured:true&include=tags')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(4)
                    .fill(postMatcher)
            });

        const jsonResponse = res.body;
        const ids = jsonResponse.posts.map(p => p.id);

        assert.equal(jsonResponse.posts.length, 4);
        assert.deepEqual(ids, [
            fixtureManager.get('posts', 4).id,
            fixtureManager.get('posts', 2).id,
            fixtureManager.get('posts', 1).id,
            fixtureManager.get('posts', 0).id
        ], 'Should have content filtered and ordered');

        jsonResponse.posts.forEach((post) => {
            if (post.featured) {
                assert.equal(post.featured, true, `Each post must either be featured or have the tag 'kitchen-sink'`);
            } else {
                const tag = post.tags
                    .map(t => t.slug)
                    .filter(s => s === 'kitchen-sink')
                    .pop();
                assert.equal(tag, 'kitchen-sink', `Each post must either be featured or have the tag 'kitchen-sink'`);
            }
        });
    });

    it('Can filter posts by authors', async function () {
        const res = await agent
            .get('posts/?filter=authors:[joe-bloggs,pat,ghost,slimer-mcectoplasm]&include=authors')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(11)
                    .fill(postMatcher)
            });

        const jsonResponse = res.body;

        assert.equal(jsonResponse.posts[0].slug, 'not-so-short-bit-complex', 'The API orders by number of matched authors');

        const primaryAuthors = jsonResponse.posts.map((post) => {
            return post.primary_author.slug;
        });
        const ghostPrimaryAuthors = primaryAuthors.filter((value) => {
            return value === 'ghost';
        });
        const joePrimaryAuthors = primaryAuthors.filter((value) => {
            return value === 'joe-bloggs';
        });

        assert.equal(ghostPrimaryAuthors.length, 7, `Each post must either have the author 'joe-bloggs' or 'ghost', 'pat' is non existing author`);
        assert.equal(joePrimaryAuthors.length, 4, `Each post must either have the author 'joe-bloggs' or 'ghost', 'pat' is non existing author`);
    });

    it('Can browse filtering by collection', async function () {
        await agent
            .get(`posts/?collection=latest`)
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: Array(11).fill(postMatcher)
            });
    });

    it('Can browse filtering by collection and using paging parameters', async function () {
        await agent
            .get(`posts/?collection=latest&limit=1&page=2`)
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: Array(1).fill(postMatcher)
            })
            .expect((res) => {
                // there are total of 11 published posts
                assert.equal(res.body.meta.pagination.total, 11);
            });
    });

    it('Can request fields of posts', async function () {
        await agent
            .get('posts/?&fields=url')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot();
    });

    it('Can include relations', async function () {
        await agent
            .get('posts/?include=tags,authors')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(11)
                    .fill(postMatcherShallowIncludes)
            });
    });

    it('Can request posts from different origin', async function () {
        await agent
            .get('posts/')
            .header('Origin', 'https://example.com')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(11)
                    .fill(postMatcher)
            });
    });

    it('Can filter by published date', async function () {
        function createFilter(publishedAt, op) {
            // This line deliberately uses double quotes because GQL cannot handle either double quotes
            // or escaped singles, see TryGhost/GQL#34
            return encodeURIComponent('published_at:' + op + '\'' + publishedAt + '\'');
        }

        const res = await agent
            .get('posts/?limit=1')
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(1)
                    .fill(postMatcher)
            });

        const post = res.body.posts[0];
        const publishedAt = moment(post.published_at).format('YYYY-MM-DD HH:mm:ss');

        const res2 = await agent
            .get(`posts/?limit=1&filter=${createFilter(publishedAt, `<`)}`)
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(1)
                    .fill(postMatcher)
            });

        const post2 = res2.body.posts[0];
        const publishedAt2 = moment(post2.published_at).format('YYYY-MM-DD HH:mm:ss');

        assert.equal(post2.title, 'Customizing your brand and design settings');

        const res3 = await agent
            .get(`posts/?limit=1&filter=${createFilter(publishedAt2, `>`)}`)
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(1)
                    .fill(postMatcher)
            });

        const post3 = res3.body.posts[0];
        assert.equal(post3.title, 'Start here for a quick overview of everything you need to know');
    });

    it('Can request a single post', async function () {
        await agent
            .get(`posts/${fixtureManager.get('posts', 0).id}/`)
            .expectStatus(200)
            .matchHeaderSnapshot({
                'content-version': anyContentVersion,
                etag: anyEtag
            })
            .matchBodySnapshot({
                posts: new Array(1)
                    .fill(postMatcher)
            });
    });

    it('Can include free and paid tiers for public post', async function () {
        const publicPost = testUtils.DataGenerator.forKnex.createPost({
            slug: 'free-to-see',
            visibility: 'public',
            published_at: moment().add(15, 'seconds').toDate() // here to ensure sorting is not modified
        });
        await models.Post.add(publicPost, {context: {internal: true}});

        const publicPostRes = await agent
            .get(`posts/${publicPost.id}/?include=tiers`)
            .expectStatus(200);
        const publicPostData = publicPostRes.body.posts[0];
        publicPostData.tiers.length.should.eql(2);
    });

    it('Can include free and paid tiers for members only post', async function () {
        const membersPost = testUtils.DataGenerator.forKnex.createPost({
            slug: 'thou-shalt-not-be-seen',
            visibility: 'members',
            published_at: moment().add(45, 'seconds').toDate() // here to ensure sorting is not modified
        });
        await models.Post.add(membersPost, {context: {internal: true}});

        const membersPostRes = await agent
            .get(`posts/${membersPost.id}/?include=tiers`)
            .expectStatus(200);
        const membersPostData = membersPostRes.body.posts[0];
        membersPostData.tiers.length.should.eql(2);
    });

    it('Can include only paid tier for paid post', async function () {
        const paidPost = testUtils.DataGenerator.forKnex.createPost({
            slug: 'thou-shalt-be-paid-for',
            visibility: 'paid',
            published_at: moment().add(30, 'seconds').toDate() // here to ensure sorting is not modified
        });
        await models.Post.add(paidPost, {context: {internal: true}});

        const paidPostRes = await agent
            .get(`posts/${paidPost.id}/?include=tiers`)
            .expectStatus(200);
        const paidPostData = paidPostRes.body.posts[0];
        paidPostData.tiers.length.should.eql(1);
    });

    it('Can include specific tier for post with tiers visibility', async function () {
        const res = await agent
            .get(`tiers/`)
            .expectStatus(200);

        const jsonResponse = res.body;
        const paidTier = jsonResponse.tiers.find(p => p.type === 'paid');

        const tiersPost = testUtils.DataGenerator.forKnex.createPost({
            slug: 'thou-shalt-be-for-specific-tiers',
            visibility: 'tiers',
            published_at: moment().add(30, 'seconds').toDate() // here to ensure sorting is not modified
        });

        tiersPost.tiers = [paidTier];

        await models.Post.add(tiersPost, {context: {internal: true}});

        const tiersPostRes = await agent
            .get(`posts/${tiersPost.id}/?include=tiers`)
            .expectStatus(200);

        const tiersPostData = tiersPostRes.body.posts[0];

        tiersPostData.tiers.length.should.eql(1);
    });

    it('Can use post excerpt as field', async function () {
        await agent
            .get(`posts/?fields=excerpt`)
            .expectStatus(200)
            .matchBodySnapshot();
    });

    it('Can use post plaintext as field', async function () {
        await agent
            .get(`posts/?fields=plaintext`)
            .expectStatus(200)
            .matchBodySnapshot();
    });

    it('Adds ?ref tags', async function () {
        const post = await models.Post.add({
            title: 'title',
            status: 'published',
            slug: 'add-ref-tags',
            mobiledoc: JSON.stringify({version: '0.3.1',atoms: [],cards: [['html',{html: '<a href="https://example.com">Link</a><a href="invalid">Test</a>'}]],markups: [],sections: [[10,0],[1,'p',[]]],ghostVersion: '4.0'})
        }, {context: {internal: true}});

        let response = await agent
            .get(`posts/${post.id}/`)
            .expectStatus(200);
        assert(response.body.posts[0].html.includes('<a href="https://example.com/?ref=127.0.0.1">Link</a><a href="invalid">Test</a>'), 'Html not expected (should contain ?ref): ' + response.body.posts[0].html);

        // Disable outbound link tracking
        mockManager.mockSetting('outbound_link_tagging', false);
        response = await agent
            .get(`posts/${post.id}/`)
            .expectStatus(200);
        assert(response.body.posts[0].html.includes('<a href="https://example.com">Link</a><a href="invalid">Test</a>'), 'Html not expected: ' + response.body.posts[0].html);
    });
});
