// express-test.js
const base = require('@playwright/test');
const {spawn} = require('child_process');
const {setupGhost, setupMailgun, setupStripe, getStripeAccountId, generateStripeIntegrationToken} = require('../utils/e2e-browser-utils');
const {buildStripeCommand} = require('../utils/stripe-cli');
const {allowStripe, mockMail, mockGeojs, assert} = require('../../utils/e2e-framework-mock-manager');
const sinon = require('sinon');
const ObjectID = require('bson-objectid').default;
const Stripe = require('stripe').Stripe;
const configUtils = require('../../utils/config-utils');
const MailgunClient = require('../../../core/server/services/lib/mailgun-client');

/* eslint-disable no-console */
const startWebhookServer = (port) => {
    const command = buildStripeCommand('listen', '--forward-connect-to', `http://127.0.0.1:${port}/members/webhooks/stripe/`);
    const webhookServer = spawn(command.split(' ')[0], command.split(' ').slice(1));

    const readyPromise = new Promise((resolve) => {
        let resolved = false;
        const onReady = () => {
            if (!resolved) {
                resolved = true;
                resolve();
            }
        };

        // Logging stdout/stderr drains the stream buffers (prevents heisenbug where
        // webhooks aren't received) while also making Stripe CLI output visible in CI
        webhookServer.stderr.on('data', (data) => {
            const msg = data.toString().trimEnd();
            console.error(`[stripe-listen:${port}] ${msg}`);
            if (msg.includes('Ready!')) {
                onReady();
            }
        });

        // Drain stdout to prevent buffer buildup, but don't log it — it's
        // per-event noise (every forwarded webhook) that drowns out useful output
        webhookServer.stdout.resume();

        webhookServer.on('error', (err) => {
            console.error(`[stripe-listen:${port}] Process error: ${err.message}`);
            onReady();
        });

        webhookServer.on('exit', (code, signal) => {
            if (!resolved) {
                console.error(`[stripe-listen:${port}] Process exited unexpectedly (code=${code}, signal=${signal})`);
            }
            onReady();
        });

        // Don't block forever if "Ready!" never appears
        setTimeout(() => {
            if (!resolved) {
                console.warn(`[stripe-listen:${port}] Timed out waiting for "Ready!" after 30s, proceeding anyway`);
                onReady();
            }
        }, 30000);
    });

    return {process: webhookServer, ready: readyPromise};
};
/* eslint-enable no-console */

/**
 * @type {import('@playwright/test').TestType<
 *      import('@playwright/test').PlaywrightTestArgs &
 *      import('@playwright/test').PlaywrightTestOptions &
 *      import('@playwright/test').PlaywrightWorkerArgs &
 *      import('@playwright/test').PlaywrightWorkerOptions
 *  >}
 * @property {import('@playwright/test').Page} sharedPage
 */
module.exports = base.test.extend({
    baseURL: async ({port, baseURL}, use) => {
        // Replace the port in baseURL with the one we got from the port fixture
        const url = new URL(baseURL);
        url.port = port.toString();
        await use(url.toString());
    },

    storageState: async ({ghost}, use) => {
        await use(ghost.state);
    },

    sharedPage: [async ({browser}, use) => {
        const page = await browser.newPage();
        await use(page);
    }, {scope: 'worker'}],

    // eslint-disable-next-line no-empty-pattern
    verificationToken: [async ({}, use) => {
        const getToken = async () => {
            const tryGetToken = () => {
                try {
                    const email = assert.sentEmail({
                        subject: /[0-9]{6} is your Ghost sign in verification code/
                    });
                    return email.subject.match(/[0-9]{6}/)[0];
                } catch (error) {
                    return null;
                }
            };

            const timeout = ms => new Promise((resolve) => {
                setTimeout(resolve, ms);
            });
            let timeoutMs = 10;
            let timeoutRetries = 5;

            while (timeoutRetries > 0) {
                const token = tryGetToken();
                if (token) {
                    return token;
                }
                await timeout(timeoutMs);
                timeoutMs *= 2;
                timeoutRetries -= 1;
            }

            return null;
        };

        await use({getToken});
    }, {scope: 'worker'}],

    // eslint-disable-next-line no-empty-pattern
    port: [async ({}, use, workerInfo) => {
        await use(2369 + workerInfo.parallelIndex);
    }, {scope: 'worker'}],

    ghost: [async ({browser, port}, use, workerInfo) => {
        // Do not initialise database before this block
        const currentDate = new Date();
        const formattedDate = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}-${String(currentDate.getHours()).padStart(2, '0')}-${String(currentDate.getMinutes()).padStart(2, '0')}-${String(currentDate.getSeconds()).padStart(2, '0')}`;
        process.env.database__connection__filename = `/tmp/ghost-playwright.${workerInfo.workerIndex}.${formattedDate}.db`;
        configUtils.set('database:connection:filename', process.env.database__connection__filename);
        configUtils.set('server:port', port);
        configUtils.set('url', `http://127.0.0.1:${port}`);
        configUtils.set('portal:url', 'http://127.0.0.1:4175/portal.min.js');
        configUtils.set('comments:url', 'http://127.0.0.1:7173/comments-ui.min.js');
        configUtils.set('signupForm:url', 'http://127.0.0.1:6174/signup-form.min.js');
        configUtils.set('announcementBar:url', 'http://127.0.0.1:4177/announcement-bar.min.js');
        configUtils.set('sodoSearch:url', 'http://127.0.0.1:4178/sodo-search.min.js');
        configUtils.set('sodoSearch:styles', 'http://127.0.0.1:4178/main.css');

        const stripeAccountId = await getStripeAccountId();
        const stripeIntegrationToken = await generateStripeIntegrationToken(stripeAccountId);

        const WebhookManager = require('../../../core/server/services/stripe/webhook-manager');
        const originalParseWebhook = WebhookManager.prototype.parseWebhook;
        const sandbox = sinon.createSandbox();
        sandbox.stub(WebhookManager.prototype, 'parseWebhook').callsFake(function (body, signature) {
            const parsedBody = JSON.parse(body);
            if (!('account' in parsedBody)) {
                throw new Error('Webhook without account');
            } else if (parsedBody.account !== stripeAccountId) {
                throw new Error('Webhook for wrong account');
            } else {
                return originalParseWebhook.call(this, body, signature);
            }
        });

        const StripeAPI = require('../../../core/server/services/stripe/stripe-api');
        const originalStripeConfigure = StripeAPI.prototype.configure;
        sandbox.stub(StripeAPI.prototype, 'configure').callsFake(function (stripeConfig) {
            originalStripeConfigure.call(this, stripeConfig);
            if (stripeConfig) {
                this._stripe = new Stripe(stripeConfig.secretKey, {
                    apiVersion: '2020-08-27',
                    stripeAccount: stripeAccountId
                });
            }
        });

        const stripeWebhook = startWebhookServer(port);
        await stripeWebhook.ready;

        if (!process.env.WEBHOOK_SECRET) {
            throw new Error('WEBHOOK_SECRET not set — did globalSetup run?');
        }

        sandbox.stub(MailgunClient.prototype, 'getInstance').returns({
            // @ts-ignore
            messages: {
                create: async function () {
                    return {
                        id: `mailgun-mock-id-${ObjectID().toHexString()}`
                    };
                }
            }
        });

        mockMail();

        const {startGhost} = require('../../utils/e2e-framework');
        const server = await startGhost({
            frontend: true,
            server: true,
            backend: true
        });

        // StartGhost automatically disables network, so we need to re-enable it for Stripe
        allowStripe();

        mockGeojs();

        const page = await browser.newPage({
            baseURL: `http://127.0.0.1:${port}/`,
            storageState: undefined
        });

        await setupGhost(page);
        await setupStripe(page, stripeIntegrationToken);
        await setupMailgun(page);
        const state = await page.context().storageState();

        await page.close();

        // Use the server in the tests.
        try {
            await use({
                server,
                state
            });
        } finally {
            const {stopGhost} = require('../../utils/e2e-utils');
            await stopGhost();
            stripeWebhook.process.kill();
            sandbox.restore();
        }
    }, {scope: 'worker', auto: true}]
});
