const {expect} = require('@playwright/test');
const test = require('../fixtures/ghost-test');
const {DateTime} = require('luxon');
const {slugify} = require('@tryghost/string');
const {createTier, createMember, createPostDraft, impersonateMember} = require('../utils');

/**
 * Test the status of a post in the post editor.
 * @param {import('@playwright/test').Page} page
 * @param {string} status The status you expect to see
 * @param {string} [hoverStatus] Optional different status when you hover the status
 */
const checkPostStatus = async (page, status, hoverStatus) => {
    await expect(page.locator('[data-test-editor-post-status]')).toContainText(status, {timeout: 5000});

    if (hoverStatus) {
        await page.locator('[data-test-editor-post-status]').hover();
        await expect(page.locator('[data-test-editor-post-status]')).toContainText(hoverStatus, {timeout: 5000});
    }
};

/**
 * Checks the post or page doesn't exist and returns a 404 page
 * @param {import('@playwright/test').Page} page
 * @param {Object} options
 * @param {String} [options.slug]
 * @param {String} [options.title]
 */
const checkPostNotPublished = async (page, {slug, title}) => {
    if (!slug) {
        slug = slugify(title);
    }
    const url = `/${slug}/`;

    // Go to the page and check if the status code is 404
    const response = await page.goto(url);
    expect(response.status()).toBe(404);
};

/**
 * Checks the post or page doesn't exist and returns a 404 page
 * @param {import('@playwright/test').Page} page
 * @param {Object} options
 * @param {String} [options.slug]
 * @param {String} options.title
 * @param {String} options.body
 */
const checkPostPublished = async (page, {slug, title, body}) => {
    if (!slug) {
        slug = slugify(title);
    }
    const url = `/${slug}/`;

    // Check again, now it should have been added to the page
    const response = await page.goto(url);
    expect(response.status()).toBe(200);

    // Check if the title and body are present on this page
    await expect(page.locator('.gh-article-title')).toHaveText(title);
    await expect(page.locator('.gh-content.gh-canvas > p')).toHaveText(body);
};

/**
 * Start a page draft with a filled in title and body. We can consider to move this to utils later.
 * @param {import('@playwright/test').Page} page
 * @param {Object} options
 * @param {String} [options.title]
 * @param {String} [options.body]
 */
const createPage = async (page, {title = 'Hello world', body = 'This is my post body.'} = {}) => {
    await page.locator('.gh-nav a[href="#/pages/"]').click();

    // Create a new post
    await page.locator('[data-test-new-page-button]').click();

    // Fill in the post title
    await page.locator('[data-test-editor-title-input]').click();
    await page.locator('[data-test-editor-title-input]').fill(title);

    // wait for editor to be ready
    await expect(page.locator('[data-lexical-editor="true"]')).toBeVisible();

    // Continue to the body by pressing enter
    await page.keyboard.press('Enter');

    await page.waitForTimeout(100); // allow new->draft switch to occur fully, without this some initial typing events can be missed
    await page.keyboard.type(body);
};

/**
 * @param {import('@playwright/test').Page} page
 */
const openPublishFlow = async (page) => {
    await page.locator('[data-test-button="publish-flow"]').first().click();
};

/**
 * @param {import('@playwright/test').Page} page
 */
const closePublishFlow = async (page) => {
    await page.locator('[data-test-button="close-publish-flow"]').click();
};

/**
 * @param {import('@playwright/test').Page} page
 */
const openPostSettingsMenu = async (page) => {
    await page.locator('[data-test-psm-trigger]').click();
};

/**
 * @param {import('@playwright/test').Page} page
 * @param {'public'|'members'|'paid'|'tiers'} visibility
 */
const setPostVisibility = async (page, visibility) => {
    await page.locator('[data-test-select="post-visibility"]').selectOption(visibility);
};

/**
 * @typedef {Object} PublishOptions
 * @property {'publish'|'publish+send'|'send'|null} [type]
 * @property {String} [recipientFilter]
 * @property {String} [newsletter]
 * @property {String} [date]
 * @property {String} [time]
 */

/**
 * Open and complete publish flow, filling in all necessary fields based on publish options
 * @param {import('@playwright/test').Page} page
 * @param {PublishOptions} options
 */
const publishPost = async (page, {type = 'publish', time, date} = {}) => {
    await openPublishFlow(page);

    // set the publish type
    if (type) {
        // Type is nullable because pages don't have a publish type button
        await page.locator('[data-test-setting="publish-type"] > button').click();
        await page.locator(`[data-test-publish-type="${type}"] + label`).click({timeout: 1000}); // If this times out, it is likely that there are no members (running a single test).
    }

    // Schedule the post
    if (date || time) {
        await page.locator('[data-test-setting="publish-at"] > button').click();
        await page.locator('[data-test-radio="schedule"] + label').click();
    }

    if (date) {
        await page.locator('[data-test-date-time-picker-date-input]').fill(date);
    }

    if (time) {
        await page.locator('[data-test-date-time-picker-time-input]').fill(time);
    }

    // TODO: set other publish options

    // continue to confirmation step
    await page.locator('[data-test-modal="publish-flow"] [data-test-button="continue"]').click();

    // TODO: assert publish flow has expected confirmation details

    // (we need force because the button is animating)
    await page.locator('[data-test-modal="publish-flow"] [data-test-button="confirm-publish"]').click({force: true});

    // TODO: assert publish flow has expected completion details
};

/**
 * When on the publish flow completed step, click the bookmark
 * @param {import('@playwright/test').Page} page
 * @returns {Promise<import('@playwright/test').Page>}
 */
const openPublishedPostBookmark = async (page) => {
    // open the published post in a new tab
    const [frontendPage] = await Promise.all([
        page.waitForEvent('popup'),
        page.locator('[data-test-complete-bookmark]').click()
    ]);

    return frontendPage;
};

test.describe('Publishing', () => {
    test.describe('Publish post', () => {
        // Post should be available on web and sent as a newsletter
        test('Publish and Email', async ({sharedPage}) => {
            const postData = {
                title: 'Publish and email post',
                body: 'This is my post body.'
            };

            // Create a member to send and email to
            await createMember(sharedPage, {email: 'test+recipient1@example.com', name: 'Publishing member'});

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);
            await publishPost(sharedPage, {type: 'publish+send'});
            await closePublishFlow(sharedPage);

            await checkPostStatus(sharedPage, 'Published');
            await checkPostPublished(sharedPage, postData);
        });

        // Post should only be available on web
        test('Publish only', async ({sharedPage}) => {
            const postData = {
                title: 'Publish post only',
                body: 'This is my post body.'
            };

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);
            await publishPost(sharedPage);
            await closePublishFlow(sharedPage);

            await checkPostStatus(sharedPage, 'Published');
            await checkPostPublished(sharedPage, postData);
        });

        // Post should be available on web and sent as a newsletter
        test('Email only', async ({sharedPage}) => {
            const postData = {
                title: 'Email only post',
                body: 'This is my post body.'
            };

            await createMember(sharedPage, {email: 'test+recipient2@example.com', name: 'Publishing member'});

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);
            await publishPost(sharedPage, {type: 'send'});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Sent to '); // can't test for 1 member for now, because depends on test ordering :( (sometimes 2 members are created)

            await checkPostNotPublished(sharedPage, postData);
        });
    });

    test.describe('Publish page', () => {
        // A page can be published and become visible on web
        test('Immediately', async ({sharedPage}) => {
            const pageData = {
                // Title should be unique to avoid slug duplicates
                title: 'Published page test',
                body: 'This is my scheduled page body.'
            };

            await sharedPage.goto('/ghost');
            await createPage(sharedPage, pageData);
            await publishPost(sharedPage, {type: null});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Published');

            // Check published
            await checkPostPublished(sharedPage, pageData);
        });

        // page should be published at the scheduled time
        test('At the scheduled time', async ({sharedPage}) => {
            const pageData = {
                // Title should be unique to avoid slug duplicates
                title: 'Scheduled sharedPage test',
                body: 'This is my scheduled sharedPage body.'
            };

            await sharedPage.goto('/ghost');
            await createPage(sharedPage, pageData);

            // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
            await publishPost(sharedPage, {time: '00:00', type: null});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published in a few seconds');

            // Go to the page and check if the status code is 404
            await checkPostNotPublished(sharedPage, pageData);

            // Now wait for 5 seconds
            await sharedPage.waitForTimeout(5000);

            // Check again, now it should have been added to the page
            await checkPostPublished(sharedPage, pageData);
        });
    });

    test.describe('Update post', () => {
        test.describe.configure({retries: 1});

        test('Can update a published post', async ({sharedPage: adminPage}) => {
            await adminPage.goto('/ghost');

            const date = DateTime.now();

            await createPostDraft(adminPage, {title: 'Testing publish update', body: 'This is the initial published text.'});
            await publishPost(adminPage);
            const frontendPage = await openPublishedPostBookmark(adminPage);
            await closePublishFlow(adminPage);
            const publishedBody = frontendPage.locator('main > article > section > p');
            const publishedHeader = frontendPage.locator('main > article > header');

            // check front-end post has the initial body text
            await expect(publishedBody).toContainText('This is the initial published text.');
            await expect(publishedHeader).toContainText(date.toFormat('LLL d, yyyy'));

            // add some extra text to the post
            await adminPage.locator('[data-kg="editor"]').click();
            await adminPage.waitForTimeout(200); //
            await adminPage.keyboard.type(' This is some updated text.');

            // change some post settings
            await openPostSettingsMenu(adminPage);
            await adminPage.fill('[data-test-date-time-picker-date-input]', '2022-01-07');
            await adminPage.fill('[data-test-field="custom-excerpt"]', 'Short description and meta');

            // save
            await adminPage.locator('[data-test-button="publish-save"]').first().click();

            // check front-end has new text after reloading
            await frontendPage.waitForTimeout(300); // let save go through
            await frontendPage.reload();
            await expect(publishedBody).toContainText('This is some updated text.');
            await expect(publishedHeader).toContainText('Jan 7, 2022');
            const metaDescription = frontendPage.locator('meta[name="description"]');
            await expect(metaDescription).toHaveAttribute('content', 'Short description and meta');
        });
    });

    test.describe('Schedule post', () => {
        // Post should be published to web and sent as a newsletter at the scheduled time
        test('Publish and Email', async ({sharedPage}) => {
            const postData = {
                // This title should be unique
                title: 'Scheduled post publish+email test',
                body: 'This is my scheduled post body.'
            };

            await createMember(sharedPage, {email: 'test+recipient3@example.com', name: 'Publishing member'});

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);

            // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
            await publishPost(sharedPage, {time: '00:00', type: 'publish+send'});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published and sent'); // Member count can differ, hence not included here
            await checkPostStatus(sharedPage, 'Scheduled', 'in a few seconds'); // Extra test for suffix on hover
            const editorUrl = await sharedPage.url();

            // Go to the homepage and check if the post is not yet visible there
            await checkPostNotPublished(sharedPage, postData);

            // Now wait 5 seconds for the scheduled post to be published
            await sharedPage.waitForTimeout(5000);

            // Check again, now it should have been added to the page
            await checkPostPublished(sharedPage, postData);

            // Check status
            await sharedPage.goto(editorUrl);
            await checkPostStatus(sharedPage, 'Published');
        });

        // Post should be published to web only at the scheduled time
        test('Publish only', async ({sharedPage}) => {
            const postData = {
                title: 'Scheduled post test',
                body: 'This is my scheduled post body.'
            };

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);

            // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
            await publishPost(sharedPage, {time: '00:00'});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published in a few seconds');
            const editorUrl = await sharedPage.url();

            // Check not published yet
            await checkPostNotPublished(sharedPage, postData);

            // Now wait 5 seconds for the scheduled post to be published
            await sharedPage.waitForTimeout(5000);

            // Check published
            await checkPostPublished(sharedPage, postData);

            // Check status
            await sharedPage.goto(editorUrl);
            await checkPostStatus(sharedPage, 'Published');
        });

        // Post should be published to web only at the scheduled time
        test('Email only', async ({sharedPage}) => {
            const postData = {
                title: 'Scheduled email only test',
                body: 'This is my scheduled post body.'
            };

            await createMember(sharedPage, {email: 'test+recipient4@example.com', name: 'Publishing member'});

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);

            // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
            await publishPost(sharedPage, {type: 'send', time: '00:00'});
            await closePublishFlow(sharedPage);
            await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent to');
            const editorUrl = await sharedPage.url();

            // Check not published yet
            await checkPostNotPublished(sharedPage, postData);

            // Now wait 5 seconds for the scheduled post to be published
            await sharedPage.waitForTimeout(5000);

            // Check status
            await sharedPage.goto(editorUrl);
            await checkPostStatus(sharedPage, 'Sent', 'Sent to');

            // Stil not published yet (email only)
            await checkPostNotPublished(sharedPage, postData);
        });

        // A previously scheduled post can be unscheduled, which resets it to a draft
        test('A scheduled post should be able to be unscheduled', async ({sharedPage, context}) => {
            const postData = {
                title: 'Unschedule post test',
                body: 'This is my unscheduled post body.'
            };

            await sharedPage.goto('/ghost');
            await createPostDraft(sharedPage, postData);

            // Schedule far in the future
            await publishPost(sharedPage, {date: '2050-01-01', time: '10:09'});
            await closePublishFlow(sharedPage);

            // Check status
            await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published at 10:09 (UTC) on 01 Jan 2050');

            // Check not published
            const testsharedPage = await context.newPage();

            // Check not published
            await checkPostNotPublished(testsharedPage, postData);

            // Now unschedule this post
            await sharedPage.locator('[data-test-button="update-flow"]').first().click();
            await sharedPage.locator('[data-test-button="revert-to-draft"]').click();

            // Check status
            await checkPostStatus(sharedPage, 'Draft - Saved');

            // Check not published
            await checkPostNotPublished(testsharedPage, postData);
        });
    });
});

test.describe('Updating post access', () => {
    test.describe('Change post visibility to members-only', () => {
        test('Only logged-in members (free or paid) can see', async ({sharedPage}) => {
            await sharedPage.goto('/ghost');

            await createPostDraft(sharedPage);
            await openPostSettingsMenu(sharedPage);
            await setPostVisibility(sharedPage, 'members');

            await publishPost(sharedPage);
            const frontendPage = await openPublishedPostBookmark(sharedPage);

            // Check if content gate for members is present on front-end
            await expect(frontendPage.locator('.gh-post-upgrade-cta-content h2')).toHaveText('This post is for subscribers only');
        });
    });

    test.describe('Change post visibility to paid-members-only', () => {
        test('Only logged-in, paid members can see', async ({sharedPage}) => {
            await sharedPage.goto('/ghost');

            await createPostDraft(sharedPage);
            await openPostSettingsMenu(sharedPage);
            await setPostVisibility(sharedPage, 'paid');

            await publishPost(sharedPage);
            const frontendPage = await openPublishedPostBookmark(sharedPage);

            // Check if content gate for paid members is present on front-end
            await expect(frontendPage.locator('.gh-post-upgrade-cta-content h2')).toHaveText('This post is for paying subscribers only');
        });
    });

    test.describe('Change post visibility to public', () => {
        test('Everyone can see', async ({sharedPage}) => {
            await sharedPage.goto('/ghost');

            await createPostDraft(sharedPage);
            await openPostSettingsMenu(sharedPage);
            await setPostVisibility(sharedPage, 'public');

            await publishPost(sharedPage);
            const frontendPage = await openPublishedPostBookmark(sharedPage);

            // Check if post content is publicly visible on front-end
            await expect(frontendPage.locator('.gh-content.gh-canvas > p')).toHaveText('This is my post body.');
        });
    });

    test('specific tiers', async ({sharedPage}) => {
        await sharedPage.goto('/ghost');

        // tiers and members are needed to test the access levels
        await createTier(sharedPage, {name: 'Silver', monthlyPrice: 5, yearlyPrice: 50});
        await createTier(sharedPage, {name: 'Gold', monthlyPrice: 10, yearlyPrice: 100});
        await createMember(sharedPage, {email: 'silver@example.com', compedPlan: 'Silver'});
        const silverMember = await sharedPage.url();
        await createMember(sharedPage, {email: 'gold@example.com', compedPlan: 'Gold'});
        const goldMember = await sharedPage.url();

        await createPostDraft(sharedPage, {body: 'Only gold members can see this'});

        await openPostSettingsMenu(sharedPage);
        await setPostVisibility(sharedPage, 'tiers');

        // backspace removes existing tiers
        await expect(sharedPage.locator('[data-test-visibility-segment-select] [data-test-selected-token]')).toHaveCount(3);
        await sharedPage.locator('[data-test-visibility-segment-select] input').click();
        await sharedPage.keyboard.press('Backspace');
        await sharedPage.waitForTimeout(50);
        await sharedPage.keyboard.press('Backspace');
        await sharedPage.waitForTimeout(50);
        await sharedPage.keyboard.press('Backspace');
        await expect(sharedPage.locator('[data-test-visibility-segment-select] [data-test-selected-token]')).toHaveCount(0);

        // specific tier can be added back on
        await sharedPage.keyboard.type('Go');
        const goldOption = sharedPage.locator('[data-test-visibility-segment-option="Gold"]');
        await goldOption.click();

        // publish
        await publishPost(sharedPage);
        const frontendPage = await openPublishedPostBookmark(sharedPage);

        // non-member doesn't have access
        await expect(frontendPage.locator('.gh-post-upgrade-cta-content h2')).toContainText('on the Gold tier only');

        // member on wrong tier doesn't have access
        await sharedPage.goto(silverMember);
        await impersonateMember(sharedPage);
        await frontendPage.reload();
        await expect(frontendPage.locator('.gh-post-upgrade-cta-content h2')).toContainText('on the Gold tier only');

        // member on selected tier has access
        await sharedPage.goto(goldMember);
        await impersonateMember(sharedPage);
        await frontendPage.reload();
        await expect(frontendPage.locator('.gh-post-upgrade-cta-content')).not.toBeVisible();
        await expect(frontendPage.locator('.gh-content.gh-canvas > p')).toHaveText('Only gold members can see this');
    });

    test('publish time in timezone', async ({page}) => {
        await page.goto('/ghost');

        await createPostDraft(page, {title: 'Published in timezones', body: 'Published in timezones'});
        await openPostSettingsMenu(page);

        // saves the post with the new date
        await expect(page.locator('[data-test-date-time-picker-timezone]')).toHaveText('UTC');
        await page.locator('[data-test-date-time-picker-datepicker]').click();
        await page.locator('.ember-power-calendar-nav-control--previous').click();
        await page.locator('.ember-power-calendar-day', {hasText: '15'}).click();
        await page.locator('[data-test-date-time-picker-time-input]').fill('12:00');

        await publishPost(page);
        await closePublishFlow(page);

        // go to settings and change the timezone
        await page.locator('[data-test-link="posts"]').click();
        await page.locator('[data-test-nav="settings"]').click();
        await expect(page.getByTestId('timezone')).toContainText('UTC');

        await page.getByTestId('timezone').getByRole('button', {name: 'Edit'}).click();
        await page.getByTestId('timezone-select').click();
        await page.locator('[data-testid="select-option"]', {hasText: 'Tokyo'}).click();

        await page.getByTestId('timezone').getByRole('button', {name: 'Save'}).click();
        await expect(page.getByTestId('timezone-select')).toBeHidden();
        await expect(page.getByTestId('timezone')).toContainText('(GMT +9:00) Osaka, Sapporo, Tokyo');

        await page.getByTestId('exit-settings').click();
        await page.locator('[data-test-nav="posts"]').click();
        await page.locator('[data-test-post-id]', {hasText: /Published in timezones/}).click();

        await openPostSettingsMenu(page);

        await expect(page.locator('[data-test-date-time-picker-timezone]')).toHaveText('JST');
        await expect(page.locator('[data-test-date-time-picker-time-input]')).toHaveValue('21:00');
        await expect(page.locator('[data-test-date-time-picker-date-input]')).toHaveValue(/-15$/);
    });

    test('default recipient settings - usually nobody', async ({page}) => {
        // switch to "usually nobody" setting
        await page.goto('/ghost/settings/newsletters');
        await page.getByTestId('default-recipients').getByRole('button', {name: 'Edit'}).click();
        await page.getByTestId('default-recipients-select').click();
        await page.locator('[data-testid="select-option"]', {hasText: /Usually nobody/}).click();
        await page.getByTestId('default-recipients').getByRole('button', {name: 'Save'}).click();

        await expect(page.getByTestId('default-recipients-select')).toBeHidden();
        await expect(page.getByTestId('default-recipients')).toContainText('Usually nobody');

        await page.goto('/ghost');

        await createMember(page, {
            name: 'Test Member Recipient',
            email: 'test@recipient.com'
        });

        // go to publish a post
        await createPostDraft(page, {title: 'Published in timezones', body: 'Published in timezones'});
        await page.locator('[data-test-button="publish-flow"]').first().click();

        await expect(page.locator('[data-test-setting="publish-type"] [data-test-setting-title]')).toContainText('Publish');

        await expect(page.locator('[data-test-setting="email-recipients"] [data-test-setting-title]')).toContainText('Not sent as newsletter');

        await page.locator('[data-test-setting="publish-type"] [data-test-setting-title]').click();

        // email-related options are enabled
        await expect(page.locator('[data-test-publish-type="publish+send"]')).not.toBeDisabled();
        await expect(page.locator('[data-test-publish-type="send"]')).not.toBeDisabled();

        await page.locator('label[for="publish-type-publish+send"]').click();

        await expect(
            page.locator('[data-test-setting="email-recipients"] [data-test-setting-title]')
        ).toContainText(/\d+\s* subscriber/m);
    });
});
