// @ts-nocheck - Models are dynamically loaded
const assert = require('node:assert/strict');
const sinon = require('sinon');
const ObjectId = require('bson-objectid').default;
const testUtils = require('../../utils');
const models = require('../../../core/server/models');
const {OUTBOX_STATUSES} = require('../../../core/server/models/outbox');
const db = require('../../../core/server/data/db');
const mailService = require('../../../core/server/services/mail');
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../../../core/server/services/member-welcome-emails/constants');

const JOB_NAME = 'process-outbox-test';
const processOutbox = require('../../../core/server/services/outbox/jobs/lib/process-outbox');

describe('Process Outbox Job', function () {
    let jobService;
    let defaultEmailDesignSettingId;

    before(async function () {
        await testUtils.startGhost();
        jobService = require('../../../core/server/services/jobs/job-service');
        defaultEmailDesignSettingId = await db.knex('email_design_settings')
            .where('slug', 'default-automated-email')
            .first('id')
            .then(row => row.id);
    });

    afterEach(async function () {
        sinon.restore();
        await db.knex('outbox').del();
        await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del();
        try {
            await jobService.removeJob(JOB_NAME);
        } catch (err) {
            // Job might not exist if test failed early
        }
    });

    async function scheduleInlineJob() {
        await jobService.addJob({
            name: JOB_NAME,
            job: () => processOutbox(),
            offloaded: false
        });

        await jobService.awaitCompletion(JOB_NAME);
    }

    describe('processes outbox entries', function () {
        beforeEach(async function () {
            sinon.stub(mailService.GhostMailer.prototype, 'send').resolves('Mail sent');

            const lexical = JSON.stringify({
                root: {
                    children: [{
                        type: 'paragraph',
                        children: [{type: 'text', text: 'Welcome to our site!'}]
                    }],
                    direction: null,
                    format: '',
                    indent: 0,
                    type: 'root',
                    version: 1
                }
            });

            const automationId = ObjectId().toHexString();
            await db.knex('automations').insert({
                id: automationId,
                status: 'active',
                name: 'Free Member Welcome Email',
                slug: MEMBER_WELCOME_EMAIL_SLUGS.free,
                created_at: new Date()
            });
            await db.knex('welcome_email_automated_emails').insert({
                id: ObjectId().toHexString(),
                welcome_email_automation_id: automationId,
                delay_days: 0,
                subject: 'Welcome to {site_title}',
                lexical,
                email_design_setting_id: defaultEmailDesignSettingId,
                created_at: new Date()
            });
        });

        it('processes pending outbox entries and deletes them after success', async function () {
            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member123',
                    email: 'test@example.com',
                    name: 'Test Member',
                    source: 'member',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING
            });

            const entriesBeforeJob = await models.Outbox.findAll();
            assert.equal(entriesBeforeJob.length, 1);

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 0);
            sinon.assert.calledOnce(mailService.GhostMailer.prototype.send);
        });

        it('does nothing when there are no pending entries', async function () {
            const entriesBeforeJob = await models.Outbox.findAll();
            assert.equal(entriesBeforeJob.length, 0);

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 0);
            sinon.assert.notCalled(mailService.GhostMailer.prototype.send);
        });

        it('processes multiple entries in a batch', async function () {
            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member1',
                    email: 'test1@example.com',
                    name: 'Test Member 1',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING
            });

            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member2',
                    email: 'test2@example.com',
                    name: 'Test Member 2',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING
            });

            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member3',
                    email: 'test3@example.com',
                    name: 'Test Member 3',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING
            });

            const entriesBeforeJob = await models.Outbox.findAll();
            assert.equal(entriesBeforeJob.length, 3);

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 0);
            sinon.assert.calledThrice(mailService.GhostMailer.prototype.send);
        });

        it('ignores entries that are not pending', async function () {
            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member1',
                    email: 'test1@example.com',
                    name: 'Test Member 1',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PROCESSING
            });

            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member2',
                    email: 'test2@example.com',
                    name: 'Test Member 2',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.FAILED
            });

            const entriesBeforeJob = await models.Outbox.findAll();
            assert.equal(entriesBeforeJob.length, 2);

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 2);
            sinon.assert.notCalled(mailService.GhostMailer.prototype.send);
        });

        it('increments retry_count and keeps entry pending when handler fails', async function () {
            mailService.GhostMailer.prototype.send.rejects(new Error('Mail service unavailable'));

            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member1',
                    email: 'retry@example.com',
                    name: 'Retry Member',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING,
                retry_count: 0
            });

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 1);

            const entry = entriesAfterJob.models[0];
            assert.equal(entry.get('status'), OUTBOX_STATUSES.PENDING);
            assert.equal(entry.get('retry_count'), 1);
            assert.ok(entry.get('message').includes('Mail service unavailable'));
        });

        it('marks entry as failed when max retries exceeded', async function () {
            mailService.GhostMailer.prototype.send.rejects(new Error('Persistent failure'));

            await models.Outbox.add({
                event_type: 'MemberCreatedEvent',
                payload: JSON.stringify({
                    memberId: 'member1',
                    email: 'maxretry@example.com',
                    name: 'Max Retry Member',
                    status: 'free'
                }),
                status: OUTBOX_STATUSES.PENDING,
                retry_count: 1
            });

            await scheduleInlineJob();

            const entriesAfterJob = await models.Outbox.findAll();
            assert.equal(entriesAfterJob.length, 1);

            const entry = entriesAfterJob.models[0];
            assert.equal(entry.get('status'), OUTBOX_STATUSES.FAILED);
            assert.equal(entry.get('retry_count'), 2);
        });
    });
});
