const assert = require('node:assert/strict');
const {assertExists, assertObjectMatches} = require('../../utils/assertions');
const sinon = require('sinon');
const testUtils = require('../../utils');
const models = require('../../../core/server/models/index');

const DEFAULT_POST_FIXTURE_COUNT = 7;

const get = require('../../../core/frontend/helpers/get');

async function createPost(data) {
    const post = testUtils.DataGenerator.forKnex.createPost(data);
    await models.Post.add(post, {context: {internal: true}});
    return post;
}

function buildMember(status, products = []) {
    return {
        uuid: '1234',
        email: 'test@example.com',
        name: 'John Doe',
        firstname: 'John',
        avatar_image: null,
        subscriptions: [],
        paid: status !== 'free',
        status: status,
        products
    };
}

function testPosts(posts, map) {
    assert(Array.isArray(posts));
    assert.equal(posts.length, DEFAULT_POST_FIXTURE_COUNT + Object.keys(map).length);

    // Free post
    for (const postID in map) {
        const expectData = map[postID];

        const post = posts.find(p => p.id === postID);
        assertExists(post);
        assertObjectMatches(post, expectData);
    }
}

describe('e2e {{#get}} helper', function () {
    let fn;
    let inverse;
    let locals = {};
    let publicPost, membersPost, paidPost, basicTierPost;

    beforeAll(async function () {
        await testUtils.startGhost({
            backend: true,
            frontend: false
        });

        publicPost = await createPost({
            slug: 'free-to-see',
            visibility: 'public',
            published_at: new Date() // here to ensure sorting is not modified
        });

        membersPost = await createPost({
            slug: 'members-post',
            visibility: 'members',
            published_at: new Date() // here to ensure sorting is not modified
        });

        paidPost = await createPost({
            slug: 'paid-to-see',
            visibility: 'paid',
            published_at: new Date() // here to ensure sorting is not modified
        });

        const defaultTier = await models.Product.findOne({slug: 'default-product'});

        basicTierPost = await createPost({
            slug: 'tiers-post',
            visibility: 'tiers',
            tiers: [{
                id: defaultTier.get('id')
            }],
            published_at: new Date() // here to ensure sorting is not modified
        });
    });

    // Assert fixtures are correct
    it('has valid fixtures', function () {
        assert.equal(publicPost.visibility, 'public');
        assert.equal(membersPost.visibility, 'members');
        assert.equal(paidPost.visibility, 'paid');
        assert.equal(basicTierPost.visibility, 'tiers');
    });

    beforeEach(function () {
        fn = sinon.spy();
        inverse = sinon.spy();
        locals = {root: {_locals: {}}};
    });

    describe('Filter optimisation', function () {
        it('Does not do filter optimisation on OR queries', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden,id:-${publicPost.id}`,
                    limit: '5'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.notEqual(fn.firstCall.args[0].meta.cacheabilityOptimisation, true);
        });
        it('Does not do filter optimisation on nested id queries', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden+(id:-${publicPost.id},id:-${membersPost.id})`,
                    limit: '5'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.notEqual(fn.firstCall.args[0].meta.cacheabilityOptimisation, true);
        });
        it('Does not do filter optimisation on multiple negated id queries', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${publicPost.id}+id:-${membersPost.id}`,
                    limit: '5'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.notEqual(fn.firstCall.args[0].meta.cacheabilityOptimisation, true);
        });
        it('Returns the correct posts with limit as a string', async function () {
            await get.call({}, 'posts', {
                hash: {
                    limit: '5'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            const firstLimit = fn.firstCall.args[0].meta.pagination.limit;
            assert.equal(firstLimit, 5);
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${firstPostUsually.id}`,
                    limit: '5'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with default limit', async function () {
            await get.call({}, 'posts', {
                hash: {},
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            const defaultLimit = fn.firstCall.args[0].meta.pagination.limit;
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${firstPostUsually.id}`
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, defaultLimit);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with a limit all', async function () {
            await get.call({}, 'posts', {
                hash: {
                    limit: 'all'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            const initialCount = fn.firstCall.args[0].posts.length;
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${firstPostUsually.id}`,
                    limit: 'all'
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].posts.length, initialCount - 1);
            // 'all' is capped to 100 since 6.x
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, 100);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with a solo negative filter', async function () {
            await get.call({}, 'posts', {
                hash: {
                    limit: 1
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${firstPostUsually.id}`,
                    limit: 5
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].posts.length, 5);
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, 5);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with a sandwiched negative filter', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden+visibility:public`,
                    limit: 1
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            await get.call({}, 'posts', {
                hash: {
                    filter: `visibility:public+id:-${firstPostUsually.id}+tag:-hash-hidden`,
                    limit: 5
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].posts.length, 5);
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, 5);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with a prefix negative filter', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden`,
                    limit: 1
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            await get.call({}, 'posts', {
                hash: {
                    filter: `id:-${firstPostUsually.id}+tag:-hash-hidden`,
                    limit: 5
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].posts.length, 5);
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, 5);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
        it('Returns the correct posts with a suffix negative filter', async function () {
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden`,
                    limit: 1
                },
                data: {},
                locals,
                fn,
                inverse
            });
            const firstPostUsually = fn.firstCall.args[0].posts[0];
            await get.call({}, 'posts', {
                hash: {
                    filter: `tag:-hash-hidden+id:-${firstPostUsually.id}`,
                    limit: 5
                },
                data: {},
                locals,
                fn,
                inverse
            });
            assert.equal(fn.secondCall.args[0].posts.length, 5);
            assert.equal(fn.secondCall.args[0].meta.pagination.limit, 5);
            assert.equal(fn.secondCall.args[0].meta.cacheabilityOptimisation, true);
            const foundFilteredPost = fn.secondCall.args[0].posts.find(post => post.id === firstPostUsually.id);
            assert.equal(foundFilteredPost, undefined);
        });
    });

    describe('{{access}} property', function () {
        let member;

        it('not authenticated member', async function () {
            member = null;
            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: false
                },
                [paidPost.id]: {
                    access: false
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });

        it('free member', async function () {
            member = buildMember('free');
            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: false
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });

        it('paid member', async function () {
            member = buildMember('paid');
            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: true
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });

        it('comped member', async function () {
            member = buildMember('comped');
            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: true
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });

        /**
         * When using the get helper, you need to include tiers to properly determine {{access}} for posts with specific tiers
         */
        it('tiers member not including tiers', async function () {
            member = buildMember('paid', [{
                name: 'Default Product',
                slug: 'default-product',
                type: 'paid',
                active: true
            }]);

            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: true
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });

        it('tiers member including tiers', async function () {
            member = buildMember('paid', [{
                name: 'Default Product',
                slug: 'default-product',
                type: 'paid',
                active: true
            }]);

            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {include: 'tiers'}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: true
                },
                [basicTierPost.id]: {
                    access: true
                }
            });
        });

        it('tiers member with different product', async function () {
            member = buildMember('paid', [{
                name: 'Default Product',
                slug: 'pro-product',
                type: 'paid',
                active: true
            }]);

            locals = {root: {_locals: {}}, member};
            await get.call(
                {},
                'posts',
                {hash: {include: 'tiers'}, data: locals, fn: fn, inverse: inverse}
            );
            testPosts(fn.firstCall.args[0].posts, {
                [publicPost.id]: {
                    access: true
                },
                [membersPost.id]: {
                    access: true
                },
                [paidPost.id]: {
                    access: true
                },
                [basicTierPost.id]: {
                    access: false
                }
            });
        });
    });
});
